@swarmclawai/swarmclaw 0.7.2 → 0.7.4

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 (274) hide show
  1. package/README.md +116 -50
  2. package/bin/package-manager.js +157 -0
  3. package/bin/package-manager.test.js +90 -0
  4. package/bin/server-cmd.js +38 -7
  5. package/bin/swarmclaw.js +54 -4
  6. package/bin/update-cmd.js +48 -10
  7. package/bin/update-cmd.test.js +55 -0
  8. package/package.json +8 -3
  9. package/scripts/postinstall.mjs +26 -0
  10. package/src/app/api/agents/[id]/route.ts +43 -0
  11. package/src/app/api/agents/[id]/thread/route.ts +39 -8
  12. package/src/app/api/agents/route.ts +35 -2
  13. package/src/app/api/auth/route.ts +77 -8
  14. package/src/app/api/chatrooms/[id]/chat/route.ts +22 -6
  15. package/src/app/api/chatrooms/[id]/pins/route.ts +2 -1
  16. package/src/app/api/chatrooms/[id]/reactions/route.ts +2 -1
  17. package/src/app/api/chatrooms/[id]/route.ts +6 -0
  18. package/src/app/api/chats/[id]/browser/route.ts +5 -1
  19. package/src/app/api/chats/[id]/chat/route.ts +7 -3
  20. package/src/app/api/chats/[id]/messages/route.ts +19 -13
  21. package/src/app/api/chats/[id]/route.ts +30 -0
  22. package/src/app/api/chats/[id]/stop/route.ts +6 -1
  23. package/src/app/api/chats/heartbeat/route.ts +2 -1
  24. package/src/app/api/chats/route.ts +23 -1
  25. package/src/app/api/connectors/[id]/doctor/route.ts +26 -0
  26. package/src/app/api/connectors/doctor/route.ts +13 -0
  27. package/src/app/api/external-agents/[id]/heartbeat/route.ts +33 -0
  28. package/src/app/api/external-agents/[id]/route.ts +31 -0
  29. package/src/app/api/external-agents/register/route.ts +3 -0
  30. package/src/app/api/external-agents/route.ts +66 -0
  31. package/src/app/api/files/open/route.ts +16 -14
  32. package/src/app/api/gateways/[id]/health/route.ts +28 -0
  33. package/src/app/api/gateways/[id]/route.ts +79 -0
  34. package/src/app/api/gateways/route.ts +57 -0
  35. package/src/app/api/memory/maintenance/route.ts +11 -1
  36. package/src/app/api/openclaw/agent-files/route.ts +27 -4
  37. package/src/app/api/openclaw/gateway/route.ts +10 -7
  38. package/src/app/api/openclaw/skills/route.ts +12 -4
  39. package/src/app/api/plugins/dependencies/route.ts +24 -0
  40. package/src/app/api/plugins/install/route.ts +15 -92
  41. package/src/app/api/plugins/route.ts +3 -26
  42. package/src/app/api/plugins/settings/route.ts +17 -12
  43. package/src/app/api/plugins/ui/route.ts +1 -0
  44. package/src/app/api/providers/[id]/discover-models/route.ts +27 -0
  45. package/src/app/api/schedules/[id]/route.ts +38 -9
  46. package/src/app/api/schedules/route.ts +51 -28
  47. package/src/app/api/settings/route.ts +55 -17
  48. package/src/app/api/setup/doctor/route.ts +6 -4
  49. package/src/app/api/tasks/[id]/route.ts +16 -6
  50. package/src/app/api/tasks/bulk/route.ts +3 -3
  51. package/src/app/api/tasks/route.ts +9 -4
  52. package/src/app/api/webhooks/[id]/route.ts +8 -1
  53. package/src/app/page.tsx +135 -17
  54. package/src/cli/binary.test.js +142 -0
  55. package/src/cli/index.js +38 -11
  56. package/src/cli/index.test.js +195 -0
  57. package/src/cli/index.ts +21 -12
  58. package/src/cli/server-cmd.test.js +59 -0
  59. package/src/cli/spec.js +20 -2
  60. package/src/components/agents/agent-card.tsx +15 -12
  61. package/src/components/agents/agent-chat-list.tsx +101 -1
  62. package/src/components/agents/agent-list.tsx +46 -9
  63. package/src/components/agents/agent-sheet.tsx +456 -23
  64. package/src/components/agents/inspector-panel.tsx +110 -49
  65. package/src/components/agents/sandbox-env-panel.tsx +4 -1
  66. package/src/components/auth/access-key-gate.tsx +36 -97
  67. package/src/components/auth/setup-wizard.tsx +970 -275
  68. package/src/components/chat/chat-area.tsx +70 -27
  69. package/src/components/chat/chat-card.tsx +6 -21
  70. package/src/components/chat/chat-header.tsx +263 -366
  71. package/src/components/chat/chat-list.tsx +62 -26
  72. package/src/components/chat/checkpoint-timeline.tsx +1 -1
  73. package/src/components/chat/message-list.tsx +145 -19
  74. package/src/components/chatrooms/chatroom-input.tsx +96 -33
  75. package/src/components/chatrooms/chatroom-list.tsx +141 -72
  76. package/src/components/chatrooms/chatroom-message.tsx +7 -6
  77. package/src/components/chatrooms/chatroom-sheet.tsx +13 -1
  78. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +5 -2
  79. package/src/components/chatrooms/chatroom-view.tsx +422 -209
  80. package/src/components/chatrooms/reaction-picker.tsx +38 -33
  81. package/src/components/connectors/connector-list.tsx +265 -127
  82. package/src/components/connectors/connector-sheet.tsx +217 -0
  83. package/src/components/gateways/gateway-sheet.tsx +567 -0
  84. package/src/components/home/home-view.tsx +128 -4
  85. package/src/components/input/chat-input.tsx +135 -86
  86. package/src/components/layout/app-layout.tsx +385 -194
  87. package/src/components/layout/mobile-header.tsx +26 -8
  88. package/src/components/memory/memory-browser.tsx +71 -6
  89. package/src/components/memory/memory-card.tsx +18 -0
  90. package/src/components/memory/memory-detail.tsx +58 -31
  91. package/src/components/memory/memory-sheet.tsx +32 -4
  92. package/src/components/plugins/plugin-list.tsx +15 -3
  93. package/src/components/plugins/plugin-sheet.tsx +118 -9
  94. package/src/components/projects/project-detail.tsx +189 -1
  95. package/src/components/providers/provider-list.tsx +158 -2
  96. package/src/components/providers/provider-sheet.tsx +81 -70
  97. package/src/components/shared/agent-picker-list.tsx +2 -2
  98. package/src/components/shared/bottom-sheet.tsx +31 -15
  99. package/src/components/shared/command-palette.tsx +111 -24
  100. package/src/components/shared/confirm-dialog.tsx +45 -30
  101. package/src/components/shared/model-combobox.tsx +90 -8
  102. package/src/components/shared/settings/plugin-manager.tsx +20 -4
  103. package/src/components/shared/settings/section-capability-policy.tsx +105 -0
  104. package/src/components/shared/settings/section-heartbeat.tsx +88 -6
  105. package/src/components/shared/settings/section-orchestrator.tsx +6 -3
  106. package/src/components/shared/settings/section-runtime-loop.tsx +5 -5
  107. package/src/components/shared/settings/section-secrets.tsx +6 -6
  108. package/src/components/shared/settings/section-user-preferences.tsx +1 -1
  109. package/src/components/shared/settings/section-voice.tsx +5 -1
  110. package/src/components/shared/settings/section-web-search.tsx +10 -2
  111. package/src/components/shared/settings/settings-page.tsx +248 -47
  112. package/src/components/tasks/approvals-panel.tsx +211 -18
  113. package/src/components/tasks/task-board.tsx +242 -46
  114. package/src/components/ui/dialog.tsx +2 -2
  115. package/src/components/usage/metrics-dashboard.tsx +74 -1
  116. package/src/components/wallets/wallet-approval-dialog.tsx +59 -54
  117. package/src/components/wallets/wallet-panel.tsx +17 -5
  118. package/src/components/webhooks/webhook-sheet.tsx +7 -7
  119. package/src/lib/auth.ts +17 -0
  120. package/src/lib/chat-streaming-state.test.ts +108 -0
  121. package/src/lib/chat-streaming-state.ts +108 -0
  122. package/src/lib/heartbeat-defaults.ts +48 -0
  123. package/src/lib/memory-presentation.ts +59 -0
  124. package/src/lib/openclaw-agent-id.test.ts +14 -0
  125. package/src/lib/openclaw-agent-id.ts +31 -0
  126. package/src/lib/provider-model-discovery-client.ts +29 -0
  127. package/src/lib/providers/index.ts +12 -5
  128. package/src/lib/runtime-loop.ts +105 -3
  129. package/src/lib/safe-storage.ts +6 -1
  130. package/src/lib/server/agent-assignment.test.ts +112 -0
  131. package/src/lib/server/agent-assignment.ts +169 -0
  132. package/src/lib/server/agent-runtime-config.test.ts +141 -0
  133. package/src/lib/server/agent-runtime-config.ts +277 -0
  134. package/src/lib/server/approval-connector-notify.test.ts +253 -0
  135. package/src/lib/server/approvals-auto-approve.test.ts +264 -0
  136. package/src/lib/server/approvals.ts +483 -75
  137. package/src/lib/server/autonomy-runtime.test.ts +341 -0
  138. package/src/lib/server/browser-state.test.ts +118 -0
  139. package/src/lib/server/browser-state.ts +123 -0
  140. package/src/lib/server/build-llm.test.ts +44 -0
  141. package/src/lib/server/build-llm.ts +11 -4
  142. package/src/lib/server/builtin-plugins.ts +34 -0
  143. package/src/lib/server/chat-execution-heartbeat.test.ts +40 -0
  144. package/src/lib/server/chat-execution-tool-events.test.ts +219 -0
  145. package/src/lib/server/chat-execution.ts +402 -125
  146. package/src/lib/server/chatroom-health.test.ts +26 -0
  147. package/src/lib/server/chatroom-health.ts +2 -3
  148. package/src/lib/server/chatroom-helpers.test.ts +74 -2
  149. package/src/lib/server/chatroom-helpers.ts +144 -11
  150. package/src/lib/server/chatroom-session-persistence.test.ts +87 -0
  151. package/src/lib/server/connectors/discord.ts +175 -11
  152. package/src/lib/server/connectors/doctor.test.ts +80 -0
  153. package/src/lib/server/connectors/doctor.ts +116 -0
  154. package/src/lib/server/connectors/manager.ts +994 -130
  155. package/src/lib/server/connectors/policy.test.ts +222 -0
  156. package/src/lib/server/connectors/policy.ts +452 -0
  157. package/src/lib/server/connectors/slack.ts +189 -10
  158. package/src/lib/server/connectors/telegram.ts +65 -15
  159. package/src/lib/server/connectors/thread-context.test.ts +44 -0
  160. package/src/lib/server/connectors/thread-context.ts +72 -0
  161. package/src/lib/server/connectors/types.ts +41 -11
  162. package/src/lib/server/daemon-state.ts +62 -3
  163. package/src/lib/server/data-dir.ts +13 -0
  164. package/src/lib/server/delegation-jobs.test.ts +140 -0
  165. package/src/lib/server/delegation-jobs.ts +248 -0
  166. package/src/lib/server/document-utils.test.ts +47 -0
  167. package/src/lib/server/document-utils.ts +397 -0
  168. package/src/lib/server/eval/agent-regression.test.ts +47 -0
  169. package/src/lib/server/eval/agent-regression.ts +1742 -0
  170. package/src/lib/server/eval/runner.ts +11 -1
  171. package/src/lib/server/eval/store.ts +2 -1
  172. package/src/lib/server/heartbeat-service.ts +23 -43
  173. package/src/lib/server/heartbeat-source.test.ts +22 -0
  174. package/src/lib/server/heartbeat-source.ts +7 -0
  175. package/src/lib/server/identity-continuity.test.ts +77 -0
  176. package/src/lib/server/identity-continuity.ts +127 -0
  177. package/src/lib/server/mailbox-utils.ts +347 -0
  178. package/src/lib/server/main-agent-loop.ts +31 -964
  179. package/src/lib/server/memory-db.ts +4 -6
  180. package/src/lib/server/memory-tiers.ts +40 -0
  181. package/src/lib/server/openclaw-agent-resolver.test.ts +70 -0
  182. package/src/lib/server/openclaw-agent-resolver.ts +128 -0
  183. package/src/lib/server/openclaw-exec-config.ts +6 -5
  184. package/src/lib/server/openclaw-gateway.ts +123 -36
  185. package/src/lib/server/openclaw-skills-normalize.test.ts +56 -0
  186. package/src/lib/server/openclaw-skills-normalize.ts +136 -0
  187. package/src/lib/server/openclaw-sync.ts +3 -2
  188. package/src/lib/server/orchestrator-lg.ts +18 -8
  189. package/src/lib/server/orchestrator.ts +5 -4
  190. package/src/lib/server/playwright-proxy.mjs +27 -3
  191. package/src/lib/server/plugins.test.ts +215 -0
  192. package/src/lib/server/plugins.ts +832 -69
  193. package/src/lib/server/provider-health.ts +33 -3
  194. package/src/lib/server/provider-model-discovery.ts +481 -0
  195. package/src/lib/server/queue.ts +4 -21
  196. package/src/lib/server/runtime-settings.test.ts +119 -0
  197. package/src/lib/server/runtime-settings.ts +12 -92
  198. package/src/lib/server/schedule-normalization.ts +187 -0
  199. package/src/lib/server/scheduler.ts +2 -0
  200. package/src/lib/server/session-archive-memory.test.ts +85 -0
  201. package/src/lib/server/session-archive-memory.ts +230 -0
  202. package/src/lib/server/session-mailbox.ts +8 -18
  203. package/src/lib/server/session-reset-policy.test.ts +99 -0
  204. package/src/lib/server/session-reset-policy.ts +311 -0
  205. package/src/lib/server/session-run-manager.ts +33 -80
  206. package/src/lib/server/session-tools/autonomy-tools.test.ts +128 -0
  207. package/src/lib/server/session-tools/calendar.ts +2 -12
  208. package/src/lib/server/session-tools/connector.ts +109 -8
  209. package/src/lib/server/session-tools/context.ts +14 -2
  210. package/src/lib/server/session-tools/crawl.ts +447 -0
  211. package/src/lib/server/session-tools/crud.ts +96 -34
  212. package/src/lib/server/session-tools/delegate-fallback.test.ts +219 -0
  213. package/src/lib/server/session-tools/delegate.ts +406 -20
  214. package/src/lib/server/session-tools/discovery-approvals.test.ts +170 -0
  215. package/src/lib/server/session-tools/discovery.ts +40 -12
  216. package/src/lib/server/session-tools/document.ts +283 -0
  217. package/src/lib/server/session-tools/email.ts +1 -3
  218. package/src/lib/server/session-tools/extract.ts +137 -0
  219. package/src/lib/server/session-tools/file-normalize.test.ts +98 -0
  220. package/src/lib/server/session-tools/file-send.test.ts +84 -1
  221. package/src/lib/server/session-tools/file.ts +243 -24
  222. package/src/lib/server/session-tools/http.ts +9 -3
  223. package/src/lib/server/session-tools/human-loop.ts +227 -0
  224. package/src/lib/server/session-tools/image-gen.ts +1 -3
  225. package/src/lib/server/session-tools/index.ts +87 -2
  226. package/src/lib/server/session-tools/mailbox.ts +276 -0
  227. package/src/lib/server/session-tools/manage-schedules.test.ts +137 -0
  228. package/src/lib/server/session-tools/memory.ts +35 -3
  229. package/src/lib/server/session-tools/monitor.ts +162 -12
  230. package/src/lib/server/session-tools/normalize-tool-args.ts +17 -14
  231. package/src/lib/server/session-tools/openclaw-nodes.test.ts +111 -0
  232. package/src/lib/server/session-tools/openclaw-nodes.ts +86 -20
  233. package/src/lib/server/session-tools/platform-normalize.test.ts +142 -0
  234. package/src/lib/server/session-tools/platform.ts +142 -4
  235. package/src/lib/server/session-tools/plugin-creator.ts +95 -25
  236. package/src/lib/server/session-tools/primitive-tools.test.ts +257 -0
  237. package/src/lib/server/session-tools/replicate.ts +1 -3
  238. package/src/lib/server/session-tools/sandbox.ts +51 -92
  239. package/src/lib/server/session-tools/schedule.ts +20 -10
  240. package/src/lib/server/session-tools/session-info.ts +58 -4
  241. package/src/lib/server/session-tools/session-tools-wiring.test.ts +54 -17
  242. package/src/lib/server/session-tools/shell.ts +2 -2
  243. package/src/lib/server/session-tools/subagent.ts +195 -27
  244. package/src/lib/server/session-tools/table.ts +587 -0
  245. package/src/lib/server/session-tools/wallet.ts +13 -10
  246. package/src/lib/server/session-tools/web-browser-config.test.ts +39 -0
  247. package/src/lib/server/session-tools/web.ts +947 -108
  248. package/src/lib/server/storage.ts +255 -10
  249. package/src/lib/server/stream-agent-chat.test.ts +61 -0
  250. package/src/lib/server/stream-agent-chat.ts +185 -25
  251. package/src/lib/server/structured-extract.test.ts +72 -0
  252. package/src/lib/server/structured-extract.ts +373 -0
  253. package/src/lib/server/task-mention.test.ts +16 -2
  254. package/src/lib/server/task-mention.ts +61 -11
  255. package/src/lib/server/tool-aliases.ts +80 -12
  256. package/src/lib/server/tool-capability-policy.ts +7 -1
  257. package/src/lib/server/tool-retry.ts +2 -0
  258. package/src/lib/server/watch-jobs.test.ts +173 -0
  259. package/src/lib/server/watch-jobs.ts +532 -0
  260. package/src/lib/server/ws-hub.ts +5 -3
  261. package/src/lib/setup-defaults.ts +352 -11
  262. package/src/lib/tool-definitions.ts +3 -4
  263. package/src/lib/validation/schemas.test.ts +26 -0
  264. package/src/lib/validation/schemas.ts +62 -1
  265. package/src/lib/ws-client.ts +14 -12
  266. package/src/proxy.ts +5 -5
  267. package/src/stores/use-app-store.ts +43 -7
  268. package/src/stores/use-chat-store.ts +31 -2
  269. package/src/stores/use-chatroom-store.ts +153 -26
  270. package/src/types/index.ts +470 -44
  271. package/src/app/api/chats/[id]/main-loop/route.ts +0 -94
  272. package/src/components/chat/new-chat-sheet.tsx +0 -253
  273. package/src/lib/server/main-session.ts +0 -17
  274. package/src/lib/server/session-run-manager.test.ts +0 -26
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs'
2
2
  import os from 'os'
3
+ import path from 'path'
3
4
  import {
4
5
  loadSessions,
5
6
  saveSessions,
@@ -27,6 +28,7 @@ import { getPluginManager } from './plugins'
27
28
  import { normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
28
29
  import { routeTaskIntent } from './capability-router'
29
30
  import { notify } from './ws-hub'
31
+ import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
30
32
  import { resolveConcreteToolPolicyBlock, resolveSessionToolPolicy } from './tool-capability-policy'
31
33
  import { pluginIdMatches } from './tool-aliases'
32
34
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
@@ -38,7 +40,12 @@ import {
38
40
  } from './llm-response-cache'
39
41
  import type { Message, MessageToolEvent, SSEEvent, UsageRecord } from '@/types'
40
42
  import { markProviderFailure, markProviderSuccess, rankDelegatesByHealth } from './provider-health'
43
+ import { isHeartbeatSource, isInternalHeartbeatRun } from './heartbeat-source'
41
44
  import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
45
+ import { buildIdentityContinuityContext, refreshSessionIdentityState } from './identity-continuity'
46
+ import { syncSessionArchiveMemory } from './session-archive-memory'
47
+ import { evaluateSessionFreshness, resetSessionRuntime, resolveSessionResetPolicy } from './session-reset-policy'
48
+ import { pruneStreamingAssistantArtifacts, upsertStreamingAssistantArtifact } from '@/lib/chat-streaming-state'
42
49
  type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
43
50
 
44
51
  /** Slice history from the most recent context-clear marker forward */
@@ -92,6 +99,10 @@ export interface ExecuteChatTurnResult {
92
99
  estimatedCost?: number
93
100
  }
94
101
 
102
+ export function shouldApplySessionFreshnessReset(source: string): boolean {
103
+ return source !== 'eval'
104
+ }
105
+
95
106
  function extractEventJson(line: string): SSEEvent | null {
96
107
  if (!line.startsWith('data: ')) return null
97
108
  try {
@@ -101,8 +112,17 @@ function extractEventJson(line: string): SSEEvent | null {
101
112
  }
102
113
  }
103
114
 
104
- function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
115
+ export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
105
116
  if (ev.t === 'tool_call') {
117
+ const previous = bag[bag.length - 1]
118
+ if (
119
+ previous
120
+ && previous.name === (ev.toolName || 'unknown')
121
+ && previous.input === (ev.toolInput || '')
122
+ && !previous.output
123
+ ) {
124
+ return
125
+ }
106
126
  bag.push({
107
127
  name: ev.toolName || 'unknown',
108
128
  input: ev.toolInput || '',
@@ -113,15 +133,210 @@ function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
113
133
  const idx = bag.findLastIndex((e) => e.name === (ev.toolName || 'unknown') && !e.output)
114
134
  if (idx === -1) return
115
135
  const output = ev.toolOutput || ''
116
- const isError = /^(Error:|error:)/i.test(output.trim())
117
- || output.includes('ECONNREFUSED')
118
- || output.includes('ETIMEDOUT')
119
- || output.includes('Error:')
120
136
  bag[idx] = {
121
137
  ...bag[idx],
122
138
  output,
123
- error: isError || undefined,
139
+ error: isLikelyToolErrorOutput(output) || undefined,
140
+ }
141
+ }
142
+ }
143
+
144
+ export function dedupeConsecutiveToolEvents(events: MessageToolEvent[]): MessageToolEvent[] {
145
+ const sameEvent = (left: MessageToolEvent, right: MessageToolEvent): boolean => (
146
+ left.name === right.name
147
+ && left.input === right.input
148
+ && (left.output || '') === (right.output || '')
149
+ && (left.error === true) === (right.error === true)
150
+ )
151
+ const sameBlock = (startA: number, startB: number, size: number): boolean => {
152
+ for (let offset = 0; offset < size; offset += 1) {
153
+ if (!sameEvent(events[startA + offset], events[startB + offset])) return false
154
+ }
155
+ return true
156
+ }
157
+
158
+ const deduped: MessageToolEvent[] = []
159
+ for (let index = 0; index < events.length;) {
160
+ const remaining = events.length - index
161
+ let collapsed = false
162
+ for (let blockSize = Math.floor(remaining / 2); blockSize >= 1; blockSize -= 1) {
163
+ if (!sameBlock(index, index + blockSize, blockSize)) continue
164
+ for (let offset = 0; offset < blockSize; offset += 1) deduped.push(events[index + offset])
165
+ const blockStart = index
166
+ index += blockSize
167
+ while (index + blockSize <= events.length && sameBlock(blockStart, index, blockSize)) {
168
+ index += blockSize
169
+ }
170
+ collapsed = true
171
+ break
172
+ }
173
+ if (collapsed) continue
174
+ deduped.push(events[index])
175
+ index += 1
176
+ }
177
+ return deduped
178
+ }
179
+
180
+ function extractDelegateResponse(outputText: string): string | null {
181
+ try {
182
+ const parsed = JSON.parse(outputText) as Record<string, unknown>
183
+ if (typeof parsed.response === 'string' && parsed.response.trim()) return parsed.response.trim()
184
+ if (typeof parsed.result === 'string' && parsed.result.trim()) return parsed.result.trim()
185
+ return null
186
+ } catch {
187
+ return null
188
+ }
189
+ }
190
+
191
+ const MANAGE_PLATFORM_RESOURCE_TO_TOOL: Record<string, string> = {
192
+ agent: 'manage_agents',
193
+ agents: 'manage_agents',
194
+ task: 'manage_tasks',
195
+ tasks: 'manage_tasks',
196
+ schedule: 'manage_schedules',
197
+ schedules: 'manage_schedules',
198
+ skill: 'manage_skills',
199
+ skills: 'manage_skills',
200
+ document: 'manage_documents',
201
+ documents: 'manage_documents',
202
+ secret: 'manage_secrets',
203
+ secrets: 'manage_secrets',
204
+ connector: 'manage_connectors',
205
+ connectors: 'manage_connectors',
206
+ session: 'manage_sessions',
207
+ sessions: 'manage_sessions',
208
+ }
209
+
210
+ export function translateRequestedToolInvocation(
211
+ requestedName: string,
212
+ rawArgs: Record<string, unknown>,
213
+ messageFallback: string,
214
+ availableToolNames?: Iterable<string>,
215
+ ): { toolName: string; args: Record<string, unknown> } {
216
+ const available = new Set(availableToolNames || [])
217
+
218
+ if (requestedName === 'web_search') {
219
+ return {
220
+ toolName: 'web',
221
+ args: {
222
+ action: 'search',
223
+ query: typeof rawArgs.query === 'string' ? rawArgs.query : messageFallback.trim(),
224
+ maxResults: typeof rawArgs.maxResults === 'number' ? rawArgs.maxResults : 5,
225
+ },
226
+ }
227
+ }
228
+ if (requestedName === 'web_fetch') {
229
+ return {
230
+ toolName: 'web',
231
+ args: {
232
+ action: 'fetch',
233
+ url: rawArgs.url,
234
+ },
235
+ }
236
+ }
237
+ if (requestedName === 'delegate_to_claude_code') {
238
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'claude' } }
239
+ }
240
+ if (requestedName === 'delegate_to_codex_cli') {
241
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'codex' } }
242
+ }
243
+ if (requestedName === 'delegate_to_opencode_cli') {
244
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'opencode' } }
245
+ }
246
+ if (requestedName === 'delegate_to_gemini_cli') {
247
+ return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
248
+ }
249
+
250
+ const managePrefix = 'manage_'
251
+ if (requestedName === 'manage_platform') {
252
+ const resource = typeof rawArgs.resource === 'string'
253
+ ? rawArgs.resource.trim().toLowerCase()
254
+ : ''
255
+ const specificTool = MANAGE_PLATFORM_RESOURCE_TO_TOOL[resource]
256
+ if (specificTool && available.has(specificTool) && !available.has('manage_platform')) {
257
+ return { toolName: specificTool, args: rawArgs }
124
258
  }
259
+ return { toolName: requestedName, args: rawArgs }
260
+ }
261
+
262
+ if (requestedName.startsWith(managePrefix) && requestedName !== 'manage_platform') {
263
+ if (!available.has(requestedName) && available.has('manage_platform')) {
264
+ const resource = requestedName.slice(managePrefix.length)
265
+ if (resource) {
266
+ const { action, id, data, ...rest } = rawArgs
267
+ const nextArgs: Record<string, unknown> = { resource, ...rest }
268
+ if (action !== undefined) nextArgs.action = action
269
+ if (id !== undefined) nextArgs.id = id
270
+ if (data !== undefined) nextArgs.data = data
271
+ return {
272
+ toolName: 'manage_platform',
273
+ args: nextArgs,
274
+ }
275
+ }
276
+ }
277
+ return { toolName: requestedName, args: rawArgs }
278
+ }
279
+
280
+ return { toolName: requestedName, args: rawArgs }
281
+ }
282
+
283
+ export function isLikelyToolErrorOutput(output: string): boolean {
284
+ const trimmed = String(output || '').trim()
285
+ if (!trimmed) return false
286
+ if (/^(Error(?::|\s*\(exit\b[^)]*\):?)|error:)/i.test(trimmed)) return true
287
+ if (/\b(MCP error|ECONNREFUSED|ETIMEDOUT|ERR_CONNECTION_REFUSED|ENOENT|EACCES)\b/i.test(trimmed)) return true
288
+ if (/\binvalid_type\b/i.test(trimmed) && /\b(issue|issues|expected|required|received|zod)\b/i.test(trimmed)) return true
289
+ try {
290
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>
291
+ const status = typeof parsed.status === 'string' ? parsed.status.trim().toLowerCase() : ''
292
+ if (status === 'error' || status === 'failed') return true
293
+ if (typeof parsed.error === 'string' && parsed.error.trim()) return true
294
+ } catch {
295
+ // Ignore non-JSON tool output.
296
+ }
297
+ return false
298
+ }
299
+
300
+ function normalizeWorkspaceSandboxLinks(text: string, cwd: string): string {
301
+ return text.replace(/\[([^\]]+)\]\(sandbox:\/workspace\/([^)]+)\)/g, (raw, label: string, relativePath: string) => {
302
+ const normalized = String(relativePath || '').replace(/^\/+/, '')
303
+ if (!normalized) return raw
304
+ const resolvedCwd = path.resolve(cwd)
305
+ const resolved = path.resolve(resolvedCwd, normalized)
306
+ if (!resolved.startsWith(resolvedCwd)) return raw
307
+ if (!fs.existsSync(resolved)) return raw
308
+ return `[${label}](/api/files/serve?path=${encodeURIComponent(resolved)})`
309
+ })
310
+ }
311
+
312
+ function normalizeAbsoluteFileMarkdownLinks(text: string): string {
313
+ return text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (raw, label: string, target: string) => {
314
+ if (!path.isAbsolute(target)) return raw
315
+ const resolved = path.resolve(target)
316
+ if (!fs.existsSync(resolved)) return raw
317
+ return `[${label}](/api/files/serve?path=${encodeURIComponent(resolved)})`
318
+ })
319
+ }
320
+
321
+ export function normalizeAssistantArtifactLinks(text: string, cwd: string): string {
322
+ const uploadsNormalized = text.replace(/sandbox:\/api\/uploads\//g, '/api/uploads/')
323
+ const workspaceNormalized = normalizeWorkspaceSandboxLinks(uploadsNormalized, cwd)
324
+ return normalizeAbsoluteFileMarkdownLinks(workspaceNormalized)
325
+ }
326
+
327
+ function extractHeartbeatStatus(text: string): { goal?: string; status?: string; summary?: string; nextAction?: string } | null {
328
+ const match = text.match(/\[AGENT_HEARTBEAT_META\]\s*(\{[^\n]*\})/i)
329
+ if (!match) return null
330
+ try {
331
+ const meta = JSON.parse(match[1]) as Record<string, unknown>
332
+ const payload: { goal?: string; status?: string; summary?: string; nextAction?: string } = {}
333
+ if (typeof meta.goal === 'string' && meta.goal.trim()) payload.goal = meta.goal.trim()
334
+ if (typeof meta.status === 'string' && meta.status.trim()) payload.status = meta.status.trim()
335
+ if (typeof meta.summary === 'string' && meta.summary.trim()) payload.summary = meta.summary.trim()
336
+ if (typeof meta.next_action === 'string' && meta.next_action.trim()) payload.nextAction = meta.next_action.trim()
337
+ return Object.keys(payload).length > 0 ? payload : null
338
+ } catch {
339
+ return null
125
340
  }
126
341
  }
127
342
 
@@ -140,7 +355,11 @@ function shouldReplaceRecentAssistantMessage(params: {
140
355
  return prevTools === 0
141
356
  }
142
357
 
143
- function requestedToolNamesFromMessage(message: string): string[] {
358
+ export function pruneSuppressedHeartbeatStreamMessage(messages: Message[]): boolean {
359
+ return pruneStreamingAssistantArtifacts(messages)
360
+ }
361
+
362
+ export function requestedToolNamesFromMessage(message: string): string[] {
144
363
  const lower = message.toLowerCase()
145
364
  const candidates = [
146
365
  'delegate_to_claude_code',
@@ -179,15 +398,24 @@ function requestedToolNamesFromMessage(message: string): string[] {
179
398
  'sandbox_list_runtimes',
180
399
  'git',
181
400
  'canvas',
182
- 'delegate',
183
401
  'schedule_wake',
184
402
  'spawn_subagent',
403
+ 'mailbox',
404
+ 'ask_human',
405
+ 'document',
406
+ 'extract',
407
+ 'table',
408
+ 'crawl',
185
409
  'context_status',
186
410
  'context_summarize',
187
411
  'openclaw_nodes',
188
412
  'openclaw_workspace',
189
413
  ]
190
- return candidates.filter((name) => lower.includes(name.toLowerCase()))
414
+ const requested = candidates.filter((name) => lower.includes(name.toLowerCase()))
415
+ if (/(^|[\s(])`delegate`([\s).,!?]|$)|\bdelegate tool\b|\buse delegate\b/.test(lower)) {
416
+ requested.push('delegate')
417
+ }
418
+ return Array.from(new Set(requested))
191
419
  }
192
420
 
193
421
  function parseKeyValueArgs(raw: string): Record<string, string> {
@@ -398,17 +626,51 @@ function syncSessionFromAgent(sessionId: string): void {
398
626
  if (!agent) return
399
627
 
400
628
  let changed = false
401
- if (agent.provider && agent.provider !== session.provider) { session.provider = agent.provider; changed = true }
402
- if (agent.model !== undefined && agent.model !== session.model) { session.model = agent.model; changed = true }
403
- if (agent.credentialId !== undefined && agent.credentialId !== session.credentialId) { session.credentialId = agent.credentialId ?? null; changed = true }
404
- if (agent.apiEndpoint !== undefined) {
405
- const normalized = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
406
- if (normalized !== session.apiEndpoint) { session.apiEndpoint = normalized; changed = true }
629
+ const route = resolvePrimaryAgentRoute(agent)
630
+ if (!session.provider && agent.provider) { session.provider = agent.provider; changed = true }
631
+ if ((session.model === undefined || session.model === null || session.model === '') && agent.model !== undefined) {
632
+ session.model = agent.model
633
+ changed = true
634
+ }
635
+ if (route) {
636
+ const resolved = applyResolvedRoute({ ...session }, route)
637
+ if (session.provider !== resolved.provider) { session.provider = resolved.provider; changed = true }
638
+ if (session.model !== resolved.model) { session.model = resolved.model; changed = true }
639
+ if ((session.credentialId || null) !== (resolved.credentialId || null)) {
640
+ session.credentialId = resolved.credentialId ?? null
641
+ changed = true
642
+ }
643
+ if (JSON.stringify(session.fallbackCredentialIds || []) !== JSON.stringify(resolved.fallbackCredentialIds || [])) {
644
+ session.fallbackCredentialIds = [...resolved.fallbackCredentialIds]
645
+ changed = true
646
+ }
647
+ if ((session.apiEndpoint || null) !== (resolved.apiEndpoint || null)) {
648
+ session.apiEndpoint = resolved.apiEndpoint ?? null
649
+ changed = true
650
+ }
651
+ if ((session.gatewayProfileId || null) !== (resolved.gatewayProfileId || null)) {
652
+ session.gatewayProfileId = resolved.gatewayProfileId ?? null
653
+ changed = true
654
+ }
655
+ } else {
656
+ if (session.credentialId === undefined && agent.credentialId !== undefined) {
657
+ session.credentialId = agent.credentialId ?? null
658
+ changed = true
659
+ }
660
+ if ((session.apiEndpoint === undefined || session.apiEndpoint === null) && agent.apiEndpoint !== undefined) {
661
+ const normalized = normalizeProviderEndpoint(agent.provider, agent.apiEndpoint ?? null)
662
+ if (normalized !== session.apiEndpoint) { session.apiEndpoint = normalized; changed = true }
663
+ }
407
664
  }
408
665
  if (!Array.isArray(session.plugins)) {
409
666
  session.plugins = Array.isArray(agent.plugins) ? [...agent.plugins] : []
410
667
  changed = true
411
668
  }
669
+ const isShortcutChat = session.shortcutForAgentId === agent.id || agent.threadSessionId === sessionId
670
+ if (isShortcutChat) {
671
+ if (session.shortcutForAgentId !== agent.id) { session.shortcutForAgentId = agent.id; changed = true }
672
+ if (session.name !== agent.name) { session.name = agent.name; changed = true }
673
+ }
412
674
 
413
675
  if (changed) {
414
676
  sessions[sessionId] = session
@@ -435,6 +697,8 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
435
697
  if (agent.description) identityLines.push(`Description: ${agent.description}`)
436
698
  identityLines.push('I should always embody this identity. I am not just an "Assistant" — I am a persistent companion with my own soul and presence.')
437
699
  parts.push(identityLines.join('\n'))
700
+ const continuityBlock = buildIdentityContinuityContext(session, agent)
701
+ if (continuityBlock) parts.push(continuityBlock)
438
702
 
439
703
  // 2. Runtime & Capabilities (OpenClaw Style)
440
704
  const runtimeLines = [
@@ -553,20 +817,56 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
553
817
  const sessions = loadSessions()
554
818
  const session = sessions[sessionId]
555
819
  if (!session) throw new Error(`Session not found: ${sessionId}`)
820
+ session.messages = Array.isArray(session.messages) ? session.messages : []
821
+ const runStartedAt = Date.now()
822
+ const runMessageStartIndex = session.messages.length
556
823
 
557
824
  const appSettings = loadSettings()
825
+ const agentForSession = session.agentId ? loadAgents()[session.agentId] : null
558
826
  const toolPolicy = resolveSessionToolPolicy(session.plugins, appSettings)
559
- const isHeartbeatRun = internal && source === 'heartbeat'
560
- const isAutoRunNoHistory = isHeartbeatRun || (internal && source === 'main-loop-followup')
561
- const heartbeatStatus = session.mainLoopState?.status || 'idle'
562
- const mainLoopIdle = session.id.startsWith('agent-thread-')
563
- && (heartbeatStatus === 'ok' || heartbeatStatus === 'idle')
564
- && !(session.mainLoopState?.pendingEvents?.length > 0)
565
- const heartbeatStatusOnly = isHeartbeatRun && mainLoopIdle
827
+ const isHeartbeatRun = isInternalHeartbeatRun(internal, source)
828
+ const isAutoRunNoHistory = isHeartbeatRun
829
+ const heartbeatStatusOnly = false
830
+ if (shouldApplySessionFreshnessReset(source)) {
831
+ const freshness = evaluateSessionFreshness({
832
+ session,
833
+ policy: resolveSessionResetPolicy({
834
+ session,
835
+ agent: agentForSession,
836
+ settings: appSettings,
837
+ }),
838
+ })
839
+ if (!freshness.fresh) {
840
+ try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* archive sync is best-effort */ }
841
+ resetSessionRuntime(session, freshness.reason || 'session_reset')
842
+ onEvent?.({ t: 'status', text: JSON.stringify({ sessionReset: freshness.reason || 'session_reset' }) })
843
+ sessions[sessionId] = session
844
+ saveSessions(sessions)
845
+ }
846
+ }
566
847
  const pluginsForRun = heartbeatStatusOnly ? [] : toolPolicy.enabledPlugins
567
848
  let sessionForRun = pluginsForRun === session.plugins
568
849
  ? session
569
850
  : { ...session, plugins: pluginsForRun }
851
+ if (agentForSession) {
852
+ const preferredRoute = resolvePrimaryAgentRoute(agentForSession)
853
+ if (preferredRoute) {
854
+ sessionForRun = applyResolvedRoute({ ...sessionForRun }, preferredRoute)
855
+ }
856
+ }
857
+ let effectiveMessage = message
858
+
859
+ if (pluginsForRun.length > 0) {
860
+ try {
861
+ effectiveMessage = await getPluginManager().transformText(
862
+ 'transformInboundMessage',
863
+ { session: sessionForRun, text: message },
864
+ { enabledIds: pluginsForRun },
865
+ )
866
+ } catch {
867
+ effectiveMessage = message
868
+ }
869
+ }
570
870
 
571
871
  // Apply model override for heartbeat runs (cheaper model)
572
872
  if (isHeartbeatRun && input.modelOverride) {
@@ -660,14 +960,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
660
960
  detail: {
661
961
  source,
662
962
  internal,
663
- provider: session.provider,
664
- model: session.model,
665
- messagePreview: message.slice(0, 200),
963
+ provider: sessionForRun.provider,
964
+ model: sessionForRun.model,
965
+ messagePreview: effectiveMessage.slice(0, 200),
666
966
  hasImage: !!(imagePath || imageUrl),
667
967
  },
668
968
  })
669
969
 
670
- const providerType = session.provider || 'claude-cli'
970
+ const providerType = sessionForRun.provider || 'claude-cli'
671
971
  const provider = getProvider(providerType)
672
972
  if (!provider) throw new Error(`Unknown provider: ${providerType}`)
673
973
 
@@ -675,11 +975,11 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
675
975
  throw new Error(`Directory not found: ${session.cwd}`)
676
976
  }
677
977
 
678
- const apiKey = resolveApiKeyForSession(session, provider)
978
+ const apiKey = resolveApiKeyForSession(sessionForRun, provider)
679
979
 
680
980
  if (!internal) {
681
981
  const linkAnalysis = await runLinkUnderstanding(message)
682
- session.messages.push({
982
+ const nextUserMessage: Message = {
683
983
  role: 'user',
684
984
  text: message,
685
985
  time: Date.now(),
@@ -687,7 +987,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
687
987
  imageUrl: imageUrl || undefined,
688
988
  attachedFiles: attachedFiles?.length ? attachedFiles : undefined,
689
989
  replyToId: input.replyToId || undefined,
690
- })
990
+ }
991
+ session.messages.push(nextUserMessage)
691
992
  if (linkAnalysis.length > 0) {
692
993
  session.messages.push({
693
994
  role: 'assistant',
@@ -698,6 +999,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
698
999
  }
699
1000
  session.lastActiveAt = Date.now()
700
1001
  saveSessions(sessions)
1002
+ try {
1003
+ await getPluginManager().runHook('onMessage', { session, message: nextUserMessage }, { enabledIds: pluginsForRun })
1004
+ } catch { /* onMessage hooks are non-critical */ }
701
1005
  }
702
1006
 
703
1007
  const systemPrompt = buildAgentSystemPrompt(session)
@@ -746,19 +1050,18 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
746
1050
  const fresh = loadSessions()
747
1051
  const current = fresh[sessionId]
748
1052
  if (!current) return
1053
+ current.messages = Array.isArray(current.messages) ? current.messages : []
749
1054
  const partialMsg: Message = {
750
1055
  role: 'assistant',
751
1056
  text: streamingPartialText,
752
1057
  time: Date.now(),
753
1058
  streaming: true,
754
- toolEvents: toolEvents.length ? [...toolEvents] : undefined,
755
- }
756
- const lastMsg = current.messages.at(-1)
757
- if (lastMsg?.streaming) {
758
- current.messages[current.messages.length - 1] = partialMsg
759
- } else {
760
- current.messages.push(partialMsg)
1059
+ toolEvents: toolEvents.length ? dedupeConsecutiveToolEvents([...toolEvents]) : undefined,
761
1060
  }
1061
+ upsertStreamingAssistantArtifact(current.messages, partialMsg, {
1062
+ minIndex: runMessageStartIndex,
1063
+ minTime: runStartedAt,
1064
+ })
762
1065
  fresh[sessionId] = current
763
1066
  saveSessions(fresh)
764
1067
  notify(`messages:${sessionId}`)
@@ -812,7 +1115,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
812
1115
  if (hasPlugins) {
813
1116
  fullResponse = (await streamAgentChat({
814
1117
  session: sessionForRun,
815
- message: message,
1118
+ message: effectiveMessage,
816
1119
  imagePath,
817
1120
  attachedFiles,
818
1121
  apiKey,
@@ -830,7 +1133,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
830
1133
  model: sessionForRun.model,
831
1134
  apiEndpoint: sessionForRun.apiEndpoint || '',
832
1135
  systemPrompt,
833
- message: message,
1136
+ message: effectiveMessage,
834
1137
  imagePath,
835
1138
  imageUrl,
836
1139
  attachedFiles,
@@ -858,7 +1161,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
858
1161
  } else {
859
1162
  fullResponse = await provider.handler.streamChat({
860
1163
  session: sessionForRun,
861
- message: message,
1164
+ message: effectiveMessage,
862
1165
  imagePath,
863
1166
  apiKey,
864
1167
  systemPrompt,
@@ -937,57 +1240,6 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
937
1240
  : null
938
1241
  const calledNames = new Set((toolEvents || []).map((t) => t.name))
939
1242
 
940
- const translateToolInvocation = (
941
- requestedName: string,
942
- rawArgs: Record<string, unknown>,
943
- ): { toolName: string; args: Record<string, unknown> } => {
944
- if (requestedName === 'web_search') {
945
- return {
946
- toolName: 'web',
947
- args: {
948
- action: 'search',
949
- query: typeof rawArgs.query === 'string' ? rawArgs.query : message.trim(),
950
- maxResults: typeof rawArgs.maxResults === 'number' ? rawArgs.maxResults : 5,
951
- },
952
- }
953
- }
954
- if (requestedName === 'web_fetch') {
955
- return {
956
- toolName: 'web',
957
- args: {
958
- action: 'fetch',
959
- url: rawArgs.url,
960
- },
961
- }
962
- }
963
- if (requestedName === 'delegate_to_claude_code') {
964
- return { toolName: 'delegate', args: { ...rawArgs, backend: 'claude' } }
965
- }
966
- if (requestedName === 'delegate_to_codex_cli') {
967
- return { toolName: 'delegate', args: { ...rawArgs, backend: 'codex' } }
968
- }
969
- if (requestedName === 'delegate_to_opencode_cli') {
970
- return { toolName: 'delegate', args: { ...rawArgs, backend: 'opencode' } }
971
- }
972
- if (requestedName === 'delegate_to_gemini_cli') {
973
- return { toolName: 'delegate', args: { ...rawArgs, backend: 'gemini' } }
974
- }
975
-
976
- const managePrefix = 'manage_'
977
- if (requestedName.startsWith(managePrefix) && requestedName !== 'manage_platform') {
978
- const resource = requestedName.slice(managePrefix.length)
979
- if (resource) {
980
- const { action, id, data, ...rest } = rawArgs
981
- return {
982
- toolName: 'manage_platform',
983
- args: { resource, action, id, data, ...rest },
984
- }
985
- }
986
- }
987
-
988
- return { toolName: requestedName, args: rawArgs }
989
- }
990
-
991
1243
  const invokeSessionTool = async (toolName: string, args: Record<string, unknown>, failurePrefix: string): Promise<boolean> => {
992
1244
  const blockedReason = resolveConcreteToolPolicyBlock(toolName, toolPolicy, appSettings)
993
1245
  if (blockedReason) {
@@ -1011,18 +1263,28 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1011
1263
  mcpDisabledTools: agent?.mcpDisabledTools,
1012
1264
  })
1013
1265
  try {
1014
- const translated = translateToolInvocation(toolName, args)
1015
- const selectedTool = tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
1266
+ const directTool = tools.find((t) => t?.name === toolName) as StructuredToolInterface | undefined
1267
+ const availableToolNames = tools.map((candidate) => candidate?.name).filter(Boolean)
1268
+ const translated = directTool
1269
+ ? { toolName, args }
1270
+ : translateRequestedToolInvocation(toolName, args, message, availableToolNames)
1271
+ const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
1016
1272
  if (!selectedTool?.invoke) return false
1017
1273
  const toolInput = JSON.stringify(translated.args)
1018
1274
  emit({ t: 'tool_call', toolName, toolInput })
1019
1275
  const toolOutput = await selectedTool.invoke(translated.args)
1020
1276
  const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
1021
1277
  emit({ t: 'tool_result', toolName, toolOutput: outputText })
1022
- // Don't overwrite fullResponse with raw tool output — it's already captured
1023
- // in toolEvents. Only set a brief notice when the LLM produced no text,
1024
- // so the message bubble isn't empty.
1025
- if (!fullResponse.trim() && outputText?.trim()) {
1278
+ const delegateResponse = (
1279
+ toolName === 'delegate'
1280
+ || toolName.startsWith('delegate_to_')
1281
+ ) ? extractDelegateResponse(outputText) : null
1282
+ if (delegateResponse) {
1283
+ fullResponse = delegateResponse
1284
+ } else if (!fullResponse.trim() && outputText?.trim()) {
1285
+ // Don't overwrite fullResponse with raw tool output — it's already captured
1286
+ // in toolEvents. Only set a brief notice when the LLM produced no text,
1287
+ // so the message bubble isn't empty.
1026
1288
  const label = toolName.replace(/_/g, ' ')
1027
1289
  fullResponse = `Used **${label}** — see tool output above for details.`
1028
1290
  }
@@ -1075,7 +1337,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1075
1337
  const delegationOrder = rankDelegatesByHealth(baseDelegationOrder as DelegateTool[])
1076
1338
  .filter((tool) => enabledDelegateTools.includes(tool))
1077
1339
  for (const delegateTool of delegationOrder) {
1078
- const invoked = await invokeSessionTool(delegateTool, { task: message.trim() }, 'Auto-delegation failed')
1340
+ const invoked = await invokeSessionTool(delegateTool, { task: effectiveMessage.trim() }, 'Auto-delegation failed')
1079
1341
  if (invoked) break
1080
1342
  }
1081
1343
  }
@@ -1095,7 +1357,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1095
1357
  for (const delegateTool of fallbackOrder) {
1096
1358
  const invoked = await invokeSessionTool(
1097
1359
  delegateTool,
1098
- { task: message.trim() },
1360
+ { task: effectiveMessage.trim() },
1099
1361
  `Provider failover via ${delegateTool} failed`,
1100
1362
  )
1101
1363
  if (invoked) {
@@ -1113,7 +1375,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1113
1375
  if (canAutoRouteWithTools && routingDecision?.intent === 'browsing' && routingDecision.primaryUrl && hasToolEnabled(sessionForRun, 'browser')) {
1114
1376
  await invokeSessionTool(
1115
1377
  'browser',
1116
- { action: 'navigate', url: routingDecision.primaryUrl },
1378
+ { action: 'read_page', url: routingDecision.primaryUrl },
1117
1379
  'Auto browser routing failed',
1118
1380
  )
1119
1381
  }
@@ -1123,7 +1385,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1123
1385
  if (routeUrl && hasToolEnabled(sessionForRun, 'web_fetch')) {
1124
1386
  await invokeSessionTool('web_fetch', { url: routeUrl }, 'Auto web_fetch routing failed')
1125
1387
  } else if (hasToolEnabled(sessionForRun, 'web_search')) {
1126
- await invokeSessionTool('web_search', { query: message.trim(), maxResults: 5 }, 'Auto web_search routing failed')
1388
+ await invokeSessionTool('web_search', { query: effectiveMessage.trim(), maxResults: 5 }, 'Auto web_search routing failed')
1127
1389
  }
1128
1390
  }
1129
1391
 
@@ -1158,27 +1420,23 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1158
1420
  errorMessage = streamErrors[streamErrors.length - 1]
1159
1421
  }
1160
1422
 
1161
- const finalText = (fullResponse || '').trim() || (!internal && errorMessage ? `Error: ${errorMessage}` : '')
1423
+ let finalText = (fullResponse || '').trim() || (!internal && errorMessage ? `Error: ${errorMessage}` : '')
1424
+ if (pluginsForRun.length > 0 && finalText && !isHeartbeatRun) {
1425
+ try {
1426
+ finalText = await getPluginManager().transformText(
1427
+ 'transformOutboundMessage',
1428
+ { session: sessionForRun, text: finalText },
1429
+ { enabledIds: pluginsForRun },
1430
+ )
1431
+ } catch { /* outbound transforms are non-critical */ }
1432
+ }
1433
+ finalText = normalizeAssistantArtifactLinks(finalText, session.cwd)
1162
1434
  const textForPersistence = stripMainLoopMetaForPersistence(finalText)
1435
+ const persistedToolEvents = dedupeConsecutiveToolEvents(toolEvents)
1163
1436
 
1164
- // Emit status SSE event from [MAIN_LOOP_META] if present
1165
- if (internal && finalText) {
1166
- const metaMatch = finalText.match(/\[MAIN_LOOP_META\]\s*(\{[^\n]*\})/i)
1167
- if (metaMatch) {
1168
- try {
1169
- const meta = JSON.parse(metaMatch[1])
1170
- const statusPayload: Record<string, string | undefined> = {}
1171
- if (meta.goal) statusPayload.goal = String(meta.goal)
1172
- if (meta.status) statusPayload.status = String(meta.status)
1173
- if (meta.summary) statusPayload.summary = String(meta.summary)
1174
- if (meta.next_action) statusPayload.nextAction = String(meta.next_action)
1175
- if (Object.keys(statusPayload).length > 0) {
1176
- emit({ t: 'status', text: JSON.stringify(statusPayload) })
1177
- }
1178
- } catch {
1179
- // ignore malformed meta JSON
1180
- }
1181
- }
1437
+ if (isHeartbeatRun && finalText) {
1438
+ const heartbeatStatus = extractHeartbeatStatus(finalText)
1439
+ if (heartbeatStatus) emit({ t: 'status', text: JSON.stringify(heartbeatStatus) })
1182
1440
  }
1183
1441
 
1184
1442
  // HEARTBEAT_OK suppression
@@ -1214,7 +1472,13 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1214
1472
  const fresh = loadSessions()
1215
1473
  const current = fresh[sessionId]
1216
1474
  if (current) {
1475
+ current.messages = Array.isArray(current.messages) ? current.messages : []
1476
+ const currentAgent = current.agentId ? loadAgents()[current.agentId] : null
1217
1477
  let changed = false
1478
+ changed = pruneStreamingAssistantArtifacts(current.messages, {
1479
+ minIndex: runMessageStartIndex,
1480
+ minTime: runStartedAt,
1481
+ }) || changed
1218
1482
  const persistField = (key: string, value: unknown) => {
1219
1483
  const normalized = normalizeResumeId(value)
1220
1484
  if ((current as Record<string, unknown>)[key] !== normalized) {
@@ -1246,7 +1510,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1246
1510
  }
1247
1511
 
1248
1512
  if (shouldPersistAssistant) {
1249
- const persistedKind = internal && source === 'heartbeat' ? 'heartbeat' : 'chat'
1513
+ const persistedKind = isHeartbeatRun ? 'heartbeat' : 'chat'
1250
1514
  const persistedText = heartbeatClassification === 'strip'
1251
1515
  ? textForPersistence.replace(/HEARTBEAT_OK/gi, '').trim()
1252
1516
  : textForPersistence
@@ -1256,13 +1520,13 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1256
1520
  text: persistedText,
1257
1521
  time: nowTs,
1258
1522
  thinking: thinkingText || undefined,
1259
- toolEvents: toolEvents.length ? toolEvents : undefined,
1523
+ toolEvents: persistedToolEvents.length ? persistedToolEvents : undefined,
1260
1524
  kind: persistedKind,
1261
1525
  }
1262
1526
  const previous = current.messages.at(-1)
1263
1527
  if (previous?.streaming || shouldReplaceRecentAssistantMessage({
1264
1528
  previous,
1265
- nextToolEvents: toolEvents,
1529
+ nextToolEvents: persistedToolEvents,
1266
1530
  nextKind: persistedKind,
1267
1531
  now: nowTs,
1268
1532
  })) {
@@ -1275,6 +1539,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1275
1539
  current.lastHeartbeatSentAt = nowTs
1276
1540
  }
1277
1541
  changed = true
1542
+ try {
1543
+ await getPluginManager().runHook('onMessage', { session: current, message: nextAssistantMessage }, { enabledIds: pluginsForRun })
1544
+ } catch { /* onMessage hooks are non-critical */ }
1278
1545
 
1279
1546
  // Conversation tone detection
1280
1547
  if (!internal) {
@@ -1329,6 +1596,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1329
1596
  }
1330
1597
  }
1331
1598
  }
1599
+ if (isHeartbeatRun && heartbeatClassification === 'suppress') {
1600
+ changed = pruneSuppressedHeartbeatStreamMessage(current.messages) || changed
1601
+ }
1332
1602
 
1333
1603
  // Fire afterChatTurn hook for all enabled plugins (memory auto-save, logging, etc.)
1334
1604
  try {
@@ -1338,13 +1608,20 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1338
1608
  response: textForPersistence,
1339
1609
  source,
1340
1610
  internal,
1341
- })
1611
+ }, { enabledIds: pluginsForRun })
1342
1612
  } catch { /* afterChatTurn hooks are non-critical */ }
1343
1613
 
1344
1614
  // Don't extend idle timeout for heartbeat runs — only user-initiated activity counts
1345
- if (source !== 'heartbeat' && source !== 'heartbeat-wake' && source !== 'main-loop-followup') {
1615
+ if (!isHeartbeatSource(source)) {
1346
1616
  current.lastActiveAt = Date.now()
1347
1617
  }
1618
+
1619
+ refreshSessionIdentityState(current, currentAgent)
1620
+ changed = true
1621
+ try {
1622
+ const archiveSync = syncSessionArchiveMemory(current, { agent: currentAgent })
1623
+ if (archiveSync.stored) changed = true
1624
+ } catch { /* archive sync is best-effort */ }
1348
1625
  fresh[sessionId] = current
1349
1626
  saveSessions(fresh)
1350
1627
  notify(`messages:${sessionId}`)
@@ -1355,7 +1632,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
1355
1632
  sessionId,
1356
1633
  text: finalText,
1357
1634
  persisted: shouldPersistAssistant,
1358
- toolEvents,
1635
+ toolEvents: persistedToolEvents,
1359
1636
  error: errorMessage,
1360
1637
  inputTokens: accumulatedUsage.inputTokens || undefined,
1361
1638
  outputTokens: accumulatedUsage.outputTokens || undefined,