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