@swarmclawai/swarmclaw 0.7.8 → 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.
- package/README.md +12 -15
- package/next.config.ts +13 -2
- package/package.json +4 -2
- package/src/app/api/agents/[id]/thread/route.ts +9 -0
- package/src/app/api/agents/route.ts +4 -0
- package/src/app/api/agents/thread-route.test.ts +133 -0
- package/src/app/api/approvals/route.test.ts +148 -0
- package/src/app/api/canvas/[sessionId]/route.ts +3 -1
- package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
- package/src/app/api/chats/[id]/devserver/route.ts +48 -7
- package/src/app/api/chats/[id]/messages/route.ts +42 -18
- package/src/app/api/chats/[id]/route.ts +1 -1
- package/src/app/api/chats/[id]/stop/route.ts +5 -4
- package/src/app/api/chats/route.ts +22 -2
- package/src/app/api/clawhub/install/route.ts +28 -8
- package/src/app/api/connectors/[id]/route.ts +26 -1
- package/src/app/api/external-agents/route.test.ts +165 -0
- package/src/app/api/gateways/[id]/health/route.ts +27 -12
- package/src/app/api/gateways/[id]/route.ts +2 -0
- package/src/app/api/gateways/health-route.test.ts +135 -0
- package/src/app/api/gateways/route.ts +2 -0
- package/src/app/api/mcp-servers/route.test.ts +130 -0
- package/src/app/api/openclaw/deploy/route.ts +38 -5
- package/src/app/api/plugins/install/route.ts +46 -6
- package/src/app/api/plugins/marketplace/route.ts +48 -15
- package/src/app/api/preview-server/route.ts +26 -11
- package/src/app/api/schedules/[id]/run/route.ts +4 -0
- package/src/app/api/schedules/route.test.ts +86 -0
- package/src/app/api/schedules/route.ts +6 -1
- package/src/app/api/setup/check-provider/route.test.ts +19 -0
- package/src/app/api/setup/check-provider/route.ts +40 -10
- package/src/app/api/skills/[id]/route.ts +12 -0
- package/src/app/api/skills/import/route.ts +14 -12
- package/src/app/api/skills/route.ts +13 -1
- package/src/app/api/tasks/[id]/route.ts +10 -1
- package/src/app/api/tasks/import/github/route.test.ts +65 -0
- package/src/app/api/tasks/import/github/route.ts +337 -0
- package/src/app/api/wallets/[id]/approve/route.ts +17 -3
- package/src/app/api/wallets/[id]/route.ts +79 -33
- package/src/app/api/wallets/[id]/send/route.ts +19 -33
- package/src/app/api/wallets/route.ts +78 -61
- package/src/app/api/webhooks/[id]/route.ts +33 -6
- package/src/app/api/webhooks/route.test.ts +272 -0
- package/src/cli/index.js +1 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-card.tsx +9 -2
- package/src/components/agents/agent-chat-list.tsx +18 -2
- package/src/components/agents/agent-list.tsx +1 -0
- package/src/components/agents/agent-sheet.tsx +73 -24
- package/src/components/agents/inspector-panel.tsx +41 -0
- package/src/components/canvas/canvas-panel.tsx +236 -65
- package/src/components/chat/chat-card.tsx +36 -13
- package/src/components/chat/chat-header.tsx +44 -16
- package/src/components/chat/chat-list.tsx +28 -4
- package/src/components/chat/checkpoint-timeline.tsx +50 -34
- package/src/components/chat/message-bubble.tsx +208 -145
- package/src/components/chat/message-list.tsx +48 -19
- package/src/components/chatrooms/chatroom-message.tsx +2 -2
- package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
- package/src/components/connectors/connector-health.tsx +1 -1
- package/src/components/connectors/connector-list.tsx +7 -2
- package/src/components/connectors/connector-sheet.tsx +337 -148
- package/src/components/gateways/gateway-sheet.tsx +2 -2
- package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
- package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
- package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
- package/src/components/plugins/plugin-list.tsx +45 -9
- package/src/components/plugins/plugin-sheet.tsx +55 -7
- package/src/components/providers/provider-list.tsx +2 -1
- package/src/components/providers/provider-sheet.tsx +21 -2
- package/src/components/schedules/schedule-card.tsx +25 -1
- package/src/components/schedules/schedule-sheet.tsx +44 -2
- package/src/components/secrets/secret-sheet.tsx +21 -2
- package/src/components/shared/agent-switch-dialog.tsx +12 -1
- package/src/components/shared/bottom-sheet.tsx +13 -3
- package/src/components/shared/command-palette.tsx +8 -1
- package/src/components/shared/confirm-dialog.tsx +19 -4
- package/src/components/shared/connector-platform-icon.test.ts +28 -0
- package/src/components/shared/connector-platform-icon.tsx +39 -6
- package/src/components/shared/settings/plugin-manager.tsx +29 -6
- package/src/components/shared/settings/section-capability-policy.tsx +7 -3
- package/src/components/skills/skill-list.tsx +25 -0
- package/src/components/skills/skill-sheet.tsx +84 -12
- package/src/components/tasks/approvals-panel.tsx +191 -95
- package/src/components/tasks/task-board.tsx +273 -2
- package/src/components/tasks/task-card.tsx +38 -9
- package/src/components/ui/dialog.tsx +2 -2
- package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
- package/src/components/wallets/wallet-panel.tsx +435 -90
- package/src/components/wallets/wallet-section.tsx +198 -48
- package/src/components/webhooks/webhook-sheet.tsx +22 -2
- package/src/lib/approval-display.ts +20 -0
- package/src/lib/canvas-content.ts +198 -0
- package/src/lib/chat-artifact-summary.ts +165 -0
- package/src/lib/chat-display.test.ts +91 -0
- package/src/lib/chat-display.ts +58 -0
- package/src/lib/chat-streaming-state.test.ts +47 -1
- package/src/lib/chat-streaming-state.ts +42 -0
- package/src/lib/ollama-model.ts +10 -0
- package/src/lib/openclaw-endpoint.test.ts +8 -0
- package/src/lib/openclaw-endpoint.ts +6 -1
- package/src/lib/plugin-install-cors.ts +46 -0
- package/src/lib/plugin-sources.test.ts +43 -0
- package/src/lib/plugin-sources.ts +77 -0
- package/src/lib/providers/ollama.ts +16 -6
- package/src/lib/providers/openclaw.test.ts +54 -0
- package/src/lib/providers/openclaw.ts +127 -11
- package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
- package/src/lib/schedule-dedupe.test.ts +66 -1
- package/src/lib/schedule-dedupe.ts +169 -12
- package/src/lib/schedule-origin.test.ts +20 -0
- package/src/lib/schedule-origin.ts +15 -0
- package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
- package/src/lib/server/agent-availability.ts +16 -0
- package/src/lib/server/agent-runtime-config.ts +12 -4
- package/src/lib/server/agent-thread-session.test.ts +51 -0
- package/src/lib/server/agent-thread-session.ts +7 -0
- package/src/lib/server/approval-match.ts +205 -0
- package/src/lib/server/approvals-auto-approve.test.ts +538 -1
- package/src/lib/server/approvals.ts +214 -1
- package/src/lib/server/assistant-control.test.ts +29 -0
- package/src/lib/server/assistant-control.ts +23 -0
- package/src/lib/server/build-llm.test.ts +79 -0
- package/src/lib/server/build-llm.ts +14 -4
- package/src/lib/server/canvas-content.test.ts +32 -0
- package/src/lib/server/canvas-content.ts +6 -0
- package/src/lib/server/capability-router.test.ts +11 -0
- package/src/lib/server/capability-router.ts +26 -1
- package/src/lib/server/chat-execution-advanced.test.ts +651 -0
- package/src/lib/server/chat-execution-disabled.test.ts +94 -0
- package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
- package/src/lib/server/chat-execution.ts +353 -72
- package/src/lib/server/clawhub-client.test.ts +14 -8
- package/src/lib/server/connectors/manager.test.ts +1147 -0
- package/src/lib/server/connectors/manager.ts +362 -63
- package/src/lib/server/connectors/pairing.ts +26 -5
- package/src/lib/server/connectors/types.ts +2 -0
- package/src/lib/server/connectors/whatsapp.test.ts +134 -0
- package/src/lib/server/connectors/whatsapp.ts +271 -47
- package/src/lib/server/context-manager.ts +6 -1
- package/src/lib/server/daemon-state.ts +1 -1
- package/src/lib/server/data-dir.test.ts +37 -0
- package/src/lib/server/data-dir.ts +20 -1
- package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
- package/src/lib/server/devserver-launch.test.ts +60 -0
- package/src/lib/server/devserver-launch.ts +85 -0
- package/src/lib/server/elevenlabs.test.ts +189 -1
- package/src/lib/server/elevenlabs.ts +147 -43
- package/src/lib/server/ethereum.ts +590 -0
- package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
- package/src/lib/server/eval/agent-regression.test.ts +18 -1
- package/src/lib/server/eval/agent-regression.ts +383 -11
- package/src/lib/server/evm-swap.ts +475 -0
- package/src/lib/server/execution-log.ts +1 -0
- package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
- package/src/lib/server/heartbeat-service.ts +15 -10
- package/src/lib/server/heartbeat-wake.test.ts +112 -0
- package/src/lib/server/heartbeat-wake.ts +338 -57
- package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
- package/src/lib/server/mcp-client.test.ts +16 -0
- package/src/lib/server/mcp-client.ts +25 -0
- package/src/lib/server/memory-integration.test.ts +719 -0
- package/src/lib/server/memory-policy.test.ts +43 -0
- package/src/lib/server/memory-policy.ts +132 -0
- package/src/lib/server/memory-tiers.test.ts +60 -0
- package/src/lib/server/memory-tiers.ts +16 -0
- package/src/lib/server/ollama-runtime.ts +58 -0
- package/src/lib/server/openclaw-deploy.test.ts +109 -1
- package/src/lib/server/openclaw-deploy.ts +557 -81
- package/src/lib/server/openclaw-gateway.test.ts +131 -0
- package/src/lib/server/openclaw-gateway.ts +10 -4
- package/src/lib/server/openclaw-health.test.ts +35 -0
- package/src/lib/server/openclaw-health.ts +215 -47
- package/src/lib/server/orchestrator-lg.ts +2 -2
- package/src/lib/server/plugins-advanced.test.ts +351 -0
- package/src/lib/server/plugins.ts +205 -5
- package/src/lib/server/queue-advanced.test.ts +528 -0
- package/src/lib/server/queue-followups.test.ts +262 -0
- package/src/lib/server/queue-reconcile.test.ts +128 -0
- package/src/lib/server/queue.ts +293 -61
- package/src/lib/server/scheduler.ts +29 -1
- package/src/lib/server/session-note.test.ts +36 -0
- package/src/lib/server/session-note.ts +42 -0
- package/src/lib/server/session-run-manager.ts +52 -4
- package/src/lib/server/session-tools/canvas.ts +14 -12
- package/src/lib/server/session-tools/connector.test.ts +138 -0
- package/src/lib/server/session-tools/connector.ts +348 -61
- package/src/lib/server/session-tools/context.ts +12 -3
- package/src/lib/server/session-tools/crud.ts +221 -10
- package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
- package/src/lib/server/session-tools/delegate.ts +64 -8
- package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
- package/src/lib/server/session-tools/discovery.ts +80 -12
- package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
- package/src/lib/server/session-tools/file.ts +43 -4
- package/src/lib/server/session-tools/human-loop.ts +35 -5
- package/src/lib/server/session-tools/index.ts +44 -9
- package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
- package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
- package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
- package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
- package/src/lib/server/session-tools/memory.test.ts +93 -0
- package/src/lib/server/session-tools/memory.ts +546 -79
- package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
- package/src/lib/server/session-tools/plugin-creator.ts +57 -1
- package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
- package/src/lib/server/session-tools/schedule.ts +6 -1
- package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
- package/src/lib/server/session-tools/shell.ts +22 -3
- package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
- package/src/lib/server/session-tools/wallet.ts +1374 -139
- package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
- package/src/lib/server/session-tools/web.ts +468 -64
- package/src/lib/server/skill-discovery.ts +128 -0
- package/src/lib/server/skill-eligibility.test.ts +84 -0
- package/src/lib/server/skill-eligibility.ts +95 -0
- package/src/lib/server/skill-prompt-budget.test.ts +102 -0
- package/src/lib/server/skill-prompt-budget.ts +125 -0
- package/src/lib/server/skills-normalize.test.ts +54 -0
- package/src/lib/server/skills-normalize.ts +372 -26
- package/src/lib/server/solana.ts +214 -29
- package/src/lib/server/storage.ts +65 -36
- package/src/lib/server/stream-agent-chat.test.ts +419 -9
- package/src/lib/server/stream-agent-chat.ts +887 -83
- package/src/lib/server/system-events.ts +1 -1
- package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
- package/src/lib/server/tool-loop-detection.test.ts +105 -0
- package/src/lib/server/tool-loop-detection.ts +260 -0
- package/src/lib/server/tool-planning.ts +4 -2
- package/src/lib/server/wallet-execution.test.ts +198 -0
- package/src/lib/server/wallet-portfolio.test.ts +98 -0
- package/src/lib/server/wallet-portfolio.ts +724 -0
- package/src/lib/server/wallet-service.test.ts +57 -0
- package/src/lib/server/wallet-service.ts +213 -0
- package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
- package/src/lib/server/watch-jobs.ts +17 -2
- package/src/lib/server/workspace-context.ts +111 -0
- package/src/lib/skill-save-payload.test.ts +39 -0
- package/src/lib/skill-save-payload.ts +37 -0
- package/src/lib/tasks.ts +28 -0
- package/src/lib/tool-event-summary.test.ts +30 -0
- package/src/lib/tool-event-summary.ts +37 -0
- package/src/lib/validation/schemas.ts +1 -0
- package/src/lib/wallet-transactions.test.ts +75 -0
- package/src/lib/wallet-transactions.ts +43 -0
- package/src/lib/wallet.test.ts +17 -0
- package/src/lib/wallet.ts +183 -0
- package/src/proxy.test.ts +31 -0
- package/src/proxy.ts +34 -2
- package/src/stores/use-chat-store.ts +15 -1
- package/src/types/index.ts +210 -14
|
@@ -25,7 +25,7 @@ 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'
|
|
@@ -47,14 +48,18 @@ 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'
|
|
49
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'
|
|
50
54
|
type DelegateTool = 'delegate_to_claude_code' | 'delegate_to_codex_cli' | 'delegate_to_opencode_cli' | 'delegate_to_gemini_cli'
|
|
51
55
|
|
|
52
56
|
/** Slice history from the most recent context-clear marker forward */
|
|
53
57
|
function applyContextClearBoundary(messages: Message[]): Message[] {
|
|
58
|
+
const filterModelHistory = (items: Message[]) => items.filter((message) => message.historyExcluded !== true)
|
|
54
59
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
55
|
-
if (messages[i].kind === 'context-clear') return messages.slice(i + 1)
|
|
60
|
+
if (messages[i].kind === 'context-clear') return filterModelHistory(messages.slice(i + 1))
|
|
56
61
|
}
|
|
57
|
-
return messages
|
|
62
|
+
return filterModelHistory(messages)
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
interface SessionWithTools {
|
|
@@ -120,6 +125,7 @@ export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
|
120
125
|
previous
|
|
121
126
|
&& previous.name === (ev.toolName || 'unknown')
|
|
122
127
|
&& previous.input === (ev.toolInput || '')
|
|
128
|
+
&& previous.toolCallId === (ev.toolCallId || previous.toolCallId)
|
|
123
129
|
&& !previous.output
|
|
124
130
|
) {
|
|
125
131
|
return
|
|
@@ -127,11 +133,14 @@ export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
|
127
133
|
bag.push({
|
|
128
134
|
name: ev.toolName || 'unknown',
|
|
129
135
|
input: ev.toolInput || '',
|
|
136
|
+
toolCallId: ev.toolCallId,
|
|
130
137
|
})
|
|
131
138
|
return
|
|
132
139
|
}
|
|
133
140
|
if (ev.t === 'tool_result') {
|
|
134
|
-
const idx =
|
|
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)
|
|
135
144
|
if (idx === -1) return
|
|
136
145
|
const output = ev.toolOutput || ''
|
|
137
146
|
bag[idx] = {
|
|
@@ -142,6 +151,25 @@ export function collectToolEvent(ev: SSEEvent, bag: MessageToolEvent[]) {
|
|
|
142
151
|
}
|
|
143
152
|
}
|
|
144
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
|
+
|
|
145
173
|
export function dedupeConsecutiveToolEvents(events: MessageToolEvent[]): MessageToolEvent[] {
|
|
146
174
|
const sameEvent = (left: MessageToolEvent, right: MessageToolEvent): boolean => (
|
|
147
175
|
left.name === right.name
|
|
@@ -178,6 +206,26 @@ export function dedupeConsecutiveToolEvents(events: MessageToolEvent[]): Message
|
|
|
178
206
|
return deduped
|
|
179
207
|
}
|
|
180
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
|
+
|
|
181
229
|
function extractDelegateResponse(outputText: string): string | null {
|
|
182
230
|
try {
|
|
183
231
|
const parsed = JSON.parse(outputText) as Record<string, unknown>
|
|
@@ -358,13 +406,32 @@ function shouldReplaceRecentAssistantMessage(params: {
|
|
|
358
406
|
return prevTools === 0
|
|
359
407
|
}
|
|
360
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
|
+
|
|
361
429
|
export function pruneSuppressedHeartbeatStreamMessage(messages: Message[]): boolean {
|
|
362
430
|
return pruneStreamingAssistantArtifacts(messages)
|
|
363
431
|
}
|
|
364
432
|
|
|
365
433
|
export function requestedToolNamesFromMessage(message: string): string[] {
|
|
366
|
-
const
|
|
367
|
-
const candidates = [
|
|
434
|
+
const explicitCandidates = [
|
|
368
435
|
'delegate_to_claude_code',
|
|
369
436
|
'delegate_to_codex_cli',
|
|
370
437
|
'delegate_to_opencode_cli',
|
|
@@ -392,35 +459,106 @@ export function requestedToolNamesFromMessage(message: string): string[] {
|
|
|
392
459
|
'wallet_tool',
|
|
393
460
|
'http_request',
|
|
394
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 = [
|
|
395
473
|
'browser',
|
|
396
474
|
'web',
|
|
397
475
|
'shell',
|
|
398
476
|
'files',
|
|
399
477
|
'edit_file',
|
|
400
|
-
'sandbox_exec',
|
|
401
|
-
'sandbox_list_runtimes',
|
|
402
478
|
'git',
|
|
403
479
|
'canvas',
|
|
404
|
-
'schedule_wake',
|
|
405
|
-
'spawn_subagent',
|
|
406
480
|
'mailbox',
|
|
407
|
-
'ask_human',
|
|
408
481
|
'document',
|
|
409
482
|
'extract',
|
|
410
483
|
'table',
|
|
411
484
|
'crawl',
|
|
412
|
-
'
|
|
413
|
-
'context_summarize',
|
|
414
|
-
'openclaw_nodes',
|
|
415
|
-
'openclaw_workspace',
|
|
485
|
+
'email',
|
|
416
486
|
]
|
|
417
|
-
const requested =
|
|
418
|
-
|
|
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')) {
|
|
419
492
|
requested.push('delegate')
|
|
420
493
|
}
|
|
421
494
|
return Array.from(new Set(requested))
|
|
422
495
|
}
|
|
423
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
|
+
|
|
424
562
|
function parseKeyValueArgs(raw: string): Record<string, string> {
|
|
425
563
|
const out: Record<string, string> = {}
|
|
426
564
|
const regex = /([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*("([^"]*)"|'([^']*)'|[^\s,]+)/g
|
|
@@ -563,6 +701,17 @@ function hasToolEnabled(session: SessionWithTools, toolName: string): boolean {
|
|
|
563
701
|
return pluginIdMatches(session?.plugins || session?.tools || [], toolName)
|
|
564
702
|
}
|
|
565
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
|
+
|
|
566
715
|
function enabledDelegationTools(session: SessionWithTools): DelegateTool[] {
|
|
567
716
|
const tools: DelegateTool[] = []
|
|
568
717
|
if (hasToolEnabled(session, 'claude_code') || hasToolEnabled(session, 'delegate')) tools.push('delegate_to_claude_code')
|
|
@@ -682,6 +831,31 @@ function syncSessionFromAgent(sessionId: string): void {
|
|
|
682
831
|
}
|
|
683
832
|
if (session.shortcutForAgentId !== agent.id) { session.shortcutForAgentId = agent.id; changed = true }
|
|
684
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
|
+
}
|
|
685
859
|
}
|
|
686
860
|
|
|
687
861
|
if (changed) {
|
|
@@ -737,6 +911,15 @@ function buildAgentSystemPrompt(session: Session): string | undefined {
|
|
|
737
911
|
}
|
|
738
912
|
}
|
|
739
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
|
+
|
|
740
923
|
// 6. Thinking & Output Format (OpenClaw Style)
|
|
741
924
|
const thinkingHint = [
|
|
742
925
|
'## Output Format',
|
|
@@ -843,8 +1026,34 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
843
1026
|
|
|
844
1027
|
const appSettings = loadSettings()
|
|
845
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
|
+
}
|
|
846
1054
|
const toolPolicy = resolveSessionToolPolicy(session.plugins, appSettings)
|
|
847
1055
|
const isHeartbeatRun = isInternalHeartbeatRun(internal, source)
|
|
1056
|
+
const isAutonomousInternalRun = internal && source !== 'chat'
|
|
848
1057
|
const isAutoRunNoHistory = isHeartbeatRun
|
|
849
1058
|
const heartbeatStatusOnly = false
|
|
850
1059
|
if (shouldApplySessionFreshnessReset(source)) {
|
|
@@ -864,6 +1073,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
864
1073
|
saveSessions(sessions)
|
|
865
1074
|
}
|
|
866
1075
|
}
|
|
1076
|
+
if (isAutonomousInternalRun) {
|
|
1077
|
+
try { syncSessionArchiveMemory(session, { agent: agentForSession }) } catch { /* archive sync is best-effort */ }
|
|
1078
|
+
}
|
|
867
1079
|
const pluginsForRun = heartbeatStatusOnly ? [] : toolPolicy.enabledPlugins
|
|
868
1080
|
let sessionForRun = pluginsForRun === session.plugins
|
|
869
1081
|
? session
|
|
@@ -1034,9 +1246,72 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1034
1246
|
|
|
1035
1247
|
let thinkingText = ''
|
|
1036
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
|
+
|
|
1037
1308
|
const emit = (ev: SSEEvent) => {
|
|
1309
|
+
let shouldPersistPartial = false
|
|
1310
|
+
let immediatePartialPersist = false
|
|
1038
1311
|
if (ev.t === 'd' && typeof ev.text === 'string') {
|
|
1039
1312
|
streamingPartialText += ev.text
|
|
1313
|
+
shouldPersistPartial = true
|
|
1314
|
+
immediatePartialPersist = streamingPartialText.length === ev.text.length
|
|
1040
1315
|
}
|
|
1041
1316
|
if (ev.t === 'err' && typeof ev.text === 'string') {
|
|
1042
1317
|
const trimmed = ev.text.trim()
|
|
@@ -1047,6 +1322,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1047
1322
|
}
|
|
1048
1323
|
if (ev.t === 'thinking' && ev.text) {
|
|
1049
1324
|
thinkingText += ev.text
|
|
1325
|
+
shouldPersistPartial = true
|
|
1050
1326
|
}
|
|
1051
1327
|
if (ev.t === 'md' && ev.text) {
|
|
1052
1328
|
try {
|
|
@@ -1060,36 +1336,18 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1060
1336
|
} catch { /* ignore non-JSON md events */ }
|
|
1061
1337
|
}
|
|
1062
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)
|
|
1063
1344
|
onEvent?.(ev)
|
|
1064
1345
|
}
|
|
1065
1346
|
|
|
1066
1347
|
// Periodic partial save so a browser refresh doesn't lose the in-flight response.
|
|
1067
|
-
|
|
1068
|
-
const PARTIAL_SAVE_INTERVAL_MS = 5000
|
|
1348
|
+
const PARTIAL_SAVE_INTERVAL_MS = 2000
|
|
1069
1349
|
const partialSaveTimer = setInterval(() => {
|
|
1070
|
-
|
|
1071
|
-
lastPartialSaveLen = streamingPartialText.length
|
|
1072
|
-
try {
|
|
1073
|
-
const fresh = loadSessions()
|
|
1074
|
-
const current = fresh[sessionId]
|
|
1075
|
-
if (!current) return
|
|
1076
|
-
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
1077
|
-
const partialMsg: Message = {
|
|
1078
|
-
role: 'assistant',
|
|
1079
|
-
text: streamingPartialText,
|
|
1080
|
-
time: Date.now(),
|
|
1081
|
-
streaming: true,
|
|
1082
|
-
toolEvents: toolEvents.length ? dedupeConsecutiveToolEvents([...toolEvents]) : undefined,
|
|
1083
|
-
}
|
|
1084
|
-
upsertStreamingAssistantArtifact(current.messages, partialMsg, {
|
|
1085
|
-
minIndex: runMessageStartIndex,
|
|
1086
|
-
minTime: runStartedAt,
|
|
1087
|
-
})
|
|
1088
|
-
fresh[sessionId] = current
|
|
1089
|
-
saveSessions(fresh)
|
|
1090
|
-
notify(`messages:${sessionId}`)
|
|
1091
|
-
} catch { /* partial save is best-effort */ }
|
|
1092
|
-
}
|
|
1350
|
+
persistStreamingAssistantArtifact()
|
|
1093
1351
|
}, PARTIAL_SAVE_INTERVAL_MS)
|
|
1094
1352
|
|
|
1095
1353
|
const parseAndEmit = (raw: string) => {
|
|
@@ -1122,7 +1380,10 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1122
1380
|
const responseCacheConfig = resolveLlmResponseCacheConfig(appSettings)
|
|
1123
1381
|
let responseCacheHit = false
|
|
1124
1382
|
let responseCacheInput: LlmResponseCacheKeyInput | null = null
|
|
1125
|
-
const
|
|
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
|
|
1126
1387
|
|
|
1127
1388
|
let durationMs = 0
|
|
1128
1389
|
const startTs = Date.now()
|
|
@@ -1134,9 +1395,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1134
1395
|
? getSessionMessages(sessionId).slice(-6)
|
|
1135
1396
|
: undefined
|
|
1136
1397
|
|
|
1137
|
-
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}`)
|
|
1138
1399
|
if (hasPlugins) {
|
|
1139
|
-
|
|
1400
|
+
const result = await streamAgentChat({
|
|
1140
1401
|
session: sessionForRun,
|
|
1141
1402
|
message: effectiveMessage,
|
|
1142
1403
|
imagePath,
|
|
@@ -1146,7 +1407,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1146
1407
|
write: (raw) => parseAndEmit(raw),
|
|
1147
1408
|
history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
|
|
1148
1409
|
signal: abortController.signal,
|
|
1149
|
-
})
|
|
1410
|
+
})
|
|
1411
|
+
fullResponse = result.finalResponse || result.fullText
|
|
1150
1412
|
} else {
|
|
1151
1413
|
const directHistorySnapshot = isAutoRunNoHistory
|
|
1152
1414
|
? getSessionMessages(sessionId).slice(-6)
|
|
@@ -1218,6 +1480,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1218
1480
|
})
|
|
1219
1481
|
} finally {
|
|
1220
1482
|
clearInterval(partialSaveTimer)
|
|
1483
|
+
if (partialSaveTimeout) clearTimeout(partialSaveTimeout)
|
|
1221
1484
|
active.delete(sessionId)
|
|
1222
1485
|
if (signal) signal.removeEventListener('abort', abortFromOutside)
|
|
1223
1486
|
}
|
|
@@ -1300,10 +1563,11 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1300
1563
|
const selectedTool = directTool || tools.find((t) => t?.name === translated.toolName) as StructuredToolInterface | undefined
|
|
1301
1564
|
if (!selectedTool?.invoke) return false
|
|
1302
1565
|
const toolInput = JSON.stringify(translated.args)
|
|
1303
|
-
|
|
1566
|
+
const toolCallId = genId()
|
|
1567
|
+
emit({ t: 'tool_call', toolName, toolInput, toolCallId })
|
|
1304
1568
|
const toolOutput = await selectedTool.invoke(translated.args)
|
|
1305
1569
|
const outputText = typeof toolOutput === 'string' ? toolOutput : JSON.stringify(toolOutput)
|
|
1306
|
-
emit({ t: 'tool_result', toolName, toolOutput: outputText })
|
|
1570
|
+
emit({ t: 'tool_result', toolName, toolOutput: outputText, toolCallId })
|
|
1307
1571
|
const delegateResponse = (
|
|
1308
1572
|
toolName === 'delegate'
|
|
1309
1573
|
|| toolName.startsWith('delegate_to_')
|
|
@@ -1357,6 +1621,9 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1357
1621
|
const shouldAutoDelegateCoding = (!internal && source === 'chat')
|
|
1358
1622
|
&& enabledDelegateTools.length > 0
|
|
1359
1623
|
&& !hasDelegationCall
|
|
1624
|
+
&& calledNames.size === 0
|
|
1625
|
+
&& !requestedToolNames.length
|
|
1626
|
+
&& !hasDirectLocalCodingTools(sessionForRun)
|
|
1360
1627
|
&& routingDecision?.intent === 'coding'
|
|
1361
1628
|
|
|
1362
1629
|
if (shouldAutoDelegateCoding) {
|
|
@@ -1445,10 +1712,28 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1445
1712
|
}
|
|
1446
1713
|
}
|
|
1447
1714
|
|
|
1448
|
-
|
|
1449
|
-
errorMessage
|
|
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
|
|
1450
1734
|
}
|
|
1451
1735
|
|
|
1736
|
+
const persistedToolEvents = dedupeConsecutiveToolEvents(toolEvents)
|
|
1452
1737
|
let finalText = (fullResponse || '').trim() || (!internal && errorMessage ? `Error: ${errorMessage}` : '')
|
|
1453
1738
|
if (pluginsForRun.length > 0 && finalText && !isHeartbeatRun) {
|
|
1454
1739
|
try {
|
|
@@ -1459,27 +1744,30 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1459
1744
|
)
|
|
1460
1745
|
} catch { /* outbound transforms are non-critical */ }
|
|
1461
1746
|
}
|
|
1747
|
+
finalText = reconcileConnectorDeliveryText(finalText, persistedToolEvents)
|
|
1462
1748
|
finalText = normalizeAssistantArtifactLinks(finalText, session.cwd)
|
|
1463
|
-
const
|
|
1464
|
-
const
|
|
1749
|
+
const rawTextForPersistence = stripMainLoopMetaForPersistence(finalText)
|
|
1750
|
+
const hiddenControlOnly = shouldSuppressHiddenControlText(rawTextForPersistence)
|
|
1751
|
+
const textForPersistence = stripHiddenControlTokens(rawTextForPersistence)
|
|
1752
|
+
const persistedText = getPersistedAssistantText(textForPersistence, persistedToolEvents)
|
|
1465
1753
|
|
|
1466
|
-
if (isHeartbeatRun &&
|
|
1467
|
-
const heartbeatStatus = extractHeartbeatStatus(
|
|
1754
|
+
if (isHeartbeatRun && rawTextForPersistence) {
|
|
1755
|
+
const heartbeatStatus = extractHeartbeatStatus(rawTextForPersistence)
|
|
1468
1756
|
if (heartbeatStatus) emit({ t: 'status', text: JSON.stringify(heartbeatStatus) })
|
|
1469
1757
|
}
|
|
1470
1758
|
|
|
1471
1759
|
// HEARTBEAT_OK suppression
|
|
1472
1760
|
const heartbeatConfig = input.heartbeatConfig
|
|
1473
1761
|
let heartbeatClassification: 'suppress' | 'strip' | 'keep' | null = null
|
|
1474
|
-
if (isHeartbeatRun &&
|
|
1475
|
-
heartbeatClassification = classifyHeartbeatResponse(
|
|
1762
|
+
if (isHeartbeatRun && rawTextForPersistence.length > 0) {
|
|
1763
|
+
heartbeatClassification = classifyHeartbeatResponse(rawTextForPersistence, heartbeatConfig?.ackMaxChars ?? 300, toolEvents.length > 0)
|
|
1476
1764
|
|
|
1477
1765
|
// Deduplication logic from OpenClaw (nagging prevention)
|
|
1478
1766
|
// If the model repeats itself exactly within 24h, suppress the heartbeat alert.
|
|
1479
1767
|
if (heartbeatClassification !== 'suppress' && !toolEvents.length) {
|
|
1480
1768
|
const prevText = session.lastHeartbeatText || ''
|
|
1481
1769
|
const prevSentAt = session.lastHeartbeatSentAt || 0
|
|
1482
|
-
const isDuplicate = prevText.trim() ===
|
|
1770
|
+
const isDuplicate = prevText.trim() === persistedText.trim()
|
|
1483
1771
|
&& (Date.now() - prevSentAt) < 24 * 60 * 60 * 1000
|
|
1484
1772
|
if (isDuplicate) {
|
|
1485
1773
|
heartbeatClassification = 'suppress'
|
|
@@ -1492,7 +1780,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1492
1780
|
notify(`heartbeat:agent:${session.agentId}`)
|
|
1493
1781
|
}
|
|
1494
1782
|
|
|
1495
|
-
const shouldPersistAssistant =
|
|
1783
|
+
const shouldPersistAssistant = !hiddenControlOnly
|
|
1784
|
+
&& hasPersistableAssistantPayload(persistedText, thinkingText, persistedToolEvents)
|
|
1496
1785
|
&& heartbeatClassification !== 'suppress'
|
|
1497
1786
|
|
|
1498
1787
|
const normalizeResumeId = (value: unknown): string | null =>
|
|
@@ -1503,16 +1792,14 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1503
1792
|
if (current) {
|
|
1504
1793
|
current.messages = Array.isArray(current.messages) ? current.messages : []
|
|
1505
1794
|
const currentAgent = current.agentId ? loadAgents()[current.agentId] : null
|
|
1506
|
-
|
|
1507
|
-
changed = pruneStreamingAssistantArtifacts(current.messages, {
|
|
1795
|
+
pruneStreamingAssistantArtifacts(current.messages, {
|
|
1508
1796
|
minIndex: runMessageStartIndex,
|
|
1509
1797
|
minTime: runStartedAt,
|
|
1510
|
-
})
|
|
1798
|
+
})
|
|
1511
1799
|
const persistField = (key: string, value: unknown) => {
|
|
1512
1800
|
const normalized = normalizeResumeId(value)
|
|
1513
1801
|
if ((current as Record<string, unknown>)[key] !== normalized) {
|
|
1514
1802
|
;(current as Record<string, unknown>)[key] = normalized
|
|
1515
|
-
changed = true
|
|
1516
1803
|
}
|
|
1517
1804
|
}
|
|
1518
1805
|
|
|
@@ -1535,15 +1822,11 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1535
1822
|
}
|
|
1536
1823
|
if (JSON.stringify(currentResume) !== JSON.stringify(nextResume)) {
|
|
1537
1824
|
current.delegateResumeIds = nextResume
|
|
1538
|
-
changed = true
|
|
1539
1825
|
}
|
|
1540
1826
|
}
|
|
1541
1827
|
|
|
1542
1828
|
if (shouldPersistAssistant) {
|
|
1543
1829
|
const persistedKind = isHeartbeatRun ? 'heartbeat' : 'chat'
|
|
1544
|
-
const persistedText = heartbeatClassification === 'strip'
|
|
1545
|
-
? textForPersistence.replace(/HEARTBEAT_OK/gi, '').trim()
|
|
1546
|
-
: textForPersistence
|
|
1547
1830
|
const nowTs = Date.now()
|
|
1548
1831
|
const nextAssistantMessage: Message = {
|
|
1549
1832
|
role: 'assistant',
|
|
@@ -1568,7 +1851,6 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1568
1851
|
current.lastHeartbeatText = persistedText
|
|
1569
1852
|
current.lastHeartbeatSentAt = nowTs
|
|
1570
1853
|
}
|
|
1571
|
-
changed = true
|
|
1572
1854
|
try {
|
|
1573
1855
|
await getPluginManager().runHook('onMessage', { session: current, message: nextAssistantMessage }, { enabledIds: pluginsForRun })
|
|
1574
1856
|
} catch { /* onMessage hooks are non-critical */ }
|
|
@@ -1627,7 +1909,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1627
1909
|
}
|
|
1628
1910
|
}
|
|
1629
1911
|
if (isHeartbeatRun && heartbeatClassification === 'suppress') {
|
|
1630
|
-
|
|
1912
|
+
pruneSuppressedHeartbeatStreamMessage(current.messages)
|
|
1631
1913
|
}
|
|
1632
1914
|
|
|
1633
1915
|
// Fire afterChatTurn hook for all enabled plugins (memory auto-save, logging, etc.)
|
|
@@ -1638,6 +1920,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1638
1920
|
response: textForPersistence,
|
|
1639
1921
|
source,
|
|
1640
1922
|
internal,
|
|
1923
|
+
toolEvents: persistedToolEvents,
|
|
1641
1924
|
}, { enabledIds: pluginsForRun })
|
|
1642
1925
|
} catch { /* afterChatTurn hooks are non-critical */ }
|
|
1643
1926
|
|
|
@@ -1647,10 +1930,8 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1647
1930
|
}
|
|
1648
1931
|
|
|
1649
1932
|
refreshSessionIdentityState(current, currentAgent)
|
|
1650
|
-
changed = true
|
|
1651
1933
|
try {
|
|
1652
|
-
|
|
1653
|
-
if (archiveSync.stored) changed = true
|
|
1934
|
+
syncSessionArchiveMemory(current, { agent: currentAgent })
|
|
1654
1935
|
} catch { /* archive sync is best-effort */ }
|
|
1655
1936
|
fresh[sessionId] = current
|
|
1656
1937
|
saveSessions(fresh)
|
|
@@ -1660,7 +1941,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1660
1941
|
return {
|
|
1661
1942
|
runId,
|
|
1662
1943
|
sessionId,
|
|
1663
|
-
text:
|
|
1944
|
+
text: hiddenControlOnly ? '' : textForPersistence,
|
|
1664
1945
|
persisted: shouldPersistAssistant,
|
|
1665
1946
|
toolEvents: persistedToolEvents,
|
|
1666
1947
|
error: errorMessage,
|