@swarmclawai/swarmclaw 1.2.0 → 1.2.1
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 +10 -0
- package/package.json +4 -1
- package/src/app/api/chats/[id]/deploy/route.ts +11 -6
- package/src/app/api/chats/[id]/devserver/route.ts +5 -2
- package/src/app/api/chats/[id]/messages/route.ts +7 -1
- package/src/app/api/credentials/[id]/route.ts +4 -1
- package/src/app/api/extensions/marketplace/route.ts +5 -2
- package/src/app/api/memory/maintenance/route.ts +5 -2
- package/src/app/api/preview-server/route.ts +14 -11
- package/src/app/api/system/status/route.ts +11 -0
- package/src/app/api/upload/route.ts +4 -1
- package/src/cli/index.js +7 -0
- package/src/cli/spec.js +1 -0
- package/src/components/agents/agent-files-editor.tsx +44 -32
- package/src/components/agents/personality-builder.tsx +13 -7
- package/src/components/agents/trash-list.tsx +1 -1
- package/src/components/chat/message-bubble.tsx +1 -0
- package/src/components/chat/message-list.tsx +25 -39
- package/src/components/chat/swarm-status-card.tsx +10 -3
- package/src/components/layout/daemon-indicator.tsx +7 -8
- package/src/components/layout/update-banner.tsx +8 -13
- package/src/components/logs/log-list.tsx +1 -1
- package/src/components/memory/memory-card.tsx +3 -1
- package/src/components/org-chart/org-chart-view.tsx +4 -0
- package/src/components/projects/project-list.tsx +4 -2
- package/src/components/projects/tabs/overview-tab.tsx +3 -2
- package/src/components/secrets/secret-sheet.tsx +1 -1
- package/src/components/secrets/secrets-list.tsx +1 -1
- package/src/components/shared/agent-switch-dialog.tsx +12 -6
- package/src/components/shared/dir-browser.tsx +22 -18
- package/src/components/skills/skill-sheet.tsx +2 -3
- package/src/components/tasks/task-list.tsx +1 -1
- package/src/components/tasks/task-sheet.tsx +1 -1
- package/src/hooks/use-openclaw-gateway.ts +46 -27
- package/src/instrumentation.ts +10 -7
- package/src/lib/chat/chat.ts +18 -2
- package/src/lib/providers/anthropic.ts +6 -3
- package/src/lib/providers/claude-cli.ts +9 -3
- package/src/lib/providers/cli-utils.ts +15 -0
- package/src/lib/providers/codex-cli.ts +9 -3
- package/src/lib/providers/gemini-cli.ts +6 -2
- package/src/lib/providers/index.ts +4 -1
- package/src/lib/providers/ollama.ts +5 -2
- package/src/lib/providers/openai.ts +8 -5
- package/src/lib/providers/opencode-cli.ts +6 -2
- package/src/lib/server/agents/agent-registry.ts +20 -3
- package/src/lib/server/agents/main-agent-loop.ts +4 -3
- package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
- package/src/lib/server/chat-execution/chat-execution.ts +14 -2
- package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
- package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
- package/src/lib/server/chat-execution/message-classifier.ts +5 -2
- package/src/lib/server/chat-execution/post-stream-finalization.ts +4 -1
- package/src/lib/server/chat-execution/prompt-builder.ts +11 -1
- package/src/lib/server/chat-execution/prompt-sections.ts +52 -9
- package/src/lib/server/chat-execution/response-completeness.ts +5 -2
- package/src/lib/server/chat-execution/stream-agent-chat.ts +42 -12
- package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
- package/src/lib/server/connectors/bluebubbles.ts +7 -4
- package/src/lib/server/connectors/connector-inbound.ts +16 -13
- package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
- package/src/lib/server/connectors/connector-outbound.ts +6 -3
- package/src/lib/server/connectors/discord.ts +10 -7
- package/src/lib/server/connectors/email.ts +17 -14
- package/src/lib/server/connectors/googlechat.ts +7 -4
- package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
- package/src/lib/server/connectors/matrix.ts +6 -3
- package/src/lib/server/connectors/openclaw.ts +20 -17
- package/src/lib/server/connectors/outbox.ts +4 -1
- package/src/lib/server/connectors/runtime-state.ts +19 -0
- package/src/lib/server/connectors/session-consolidation.ts +5 -2
- package/src/lib/server/connectors/signal.ts +9 -6
- package/src/lib/server/connectors/slack.ts +13 -10
- package/src/lib/server/connectors/teams.ts +8 -5
- package/src/lib/server/connectors/telegram.ts +15 -12
- package/src/lib/server/connectors/whatsapp.ts +32 -29
- package/src/lib/server/embeddings.ts +4 -1
- package/src/lib/server/link-understanding.ts +4 -1
- package/src/lib/server/memory/memory-abstract.ts +59 -0
- package/src/lib/server/memory/memory-db.ts +40 -14
- package/src/lib/server/missions/mission-service.ts +6 -3
- package/src/lib/server/openclaw/gateway.ts +8 -5
- package/src/lib/server/project-utils.ts +13 -0
- package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
- package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
- package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
- package/src/lib/server/provider-health.ts +18 -0
- package/src/lib/server/query-expansion.ts +4 -1
- package/src/lib/server/runtime/alert-dispatch.ts +7 -6
- package/src/lib/server/runtime/daemon-state.ts +189 -50
- package/src/lib/server/runtime/heartbeat-service.ts +23 -0
- package/src/lib/server/runtime/idle-window.ts +4 -1
- package/src/lib/server/runtime/perf.ts +4 -1
- package/src/lib/server/runtime/process-manager.ts +7 -4
- package/src/lib/server/runtime/queue.ts +31 -28
- package/src/lib/server/runtime/scheduler.ts +9 -6
- package/src/lib/server/runtime/session-run-manager.ts +3 -0
- package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
- package/src/lib/server/sandbox/novnc-auth.ts +10 -0
- package/src/lib/server/session-tools/context.ts +14 -0
- package/src/lib/server/session-tools/discovery.ts +9 -6
- package/src/lib/server/session-tools/index.ts +3 -1
- package/src/lib/server/session-tools/platform.ts +1 -1
- package/src/lib/server/session-tools/subagent.ts +23 -2
- package/src/lib/server/session-tools/wallet.ts +4 -1
- package/src/lib/server/skills/clawhub-client.ts +4 -1
- package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
- package/src/lib/server/skills/skill-eligibility.ts +6 -0
- package/src/lib/server/solana.ts +6 -0
- package/src/lib/server/storage-auth.ts +5 -5
- package/src/lib/server/storage-normalization.ts +4 -0
- package/src/lib/server/storage.ts +19 -8
- package/src/lib/server/tasks/task-followups.ts +4 -1
- package/src/lib/server/tool-loop-detection.ts +8 -3
- package/src/lib/server/tool-planning.ts +226 -0
- package/src/lib/server/tool-retry.ts +4 -3
- package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
- package/src/lib/server/ws-hub.ts +5 -2
- package/src/lib/strip-internal-metadata.test.ts +44 -4
- package/src/lib/strip-internal-metadata.ts +20 -6
- package/src/stores/use-approval-store.ts +7 -1
- package/src/stores/use-chat-store.ts +5 -1
- package/src/types/index.ts +6 -0
|
@@ -8,8 +8,11 @@ import { streamAnthropicChat } from './anthropic'
|
|
|
8
8
|
import { streamOpenClawChat } from './openclaw'
|
|
9
9
|
import { errorMessage, sleep, jitteredBackoff } from '@/lib/shared-utils'
|
|
10
10
|
import { classifyProviderError } from './error-classification'
|
|
11
|
+
import { log } from '@/lib/server/logger'
|
|
11
12
|
import type { ProviderInfo, ProviderConfig as CustomProviderConfig, ProviderType } from '../../types'
|
|
12
13
|
|
|
14
|
+
const TAG = 'providers'
|
|
15
|
+
|
|
13
16
|
export interface ProviderHandler {
|
|
14
17
|
streamChat: (opts: StreamChatOptions) => Promise<string>
|
|
15
18
|
}
|
|
@@ -439,7 +442,7 @@ export async function streamChatWithFailover(
|
|
|
439
442
|
if (classified.reason === 'auth_permanent') throw err
|
|
440
443
|
|
|
441
444
|
if (i < credentialIds.length - 1) {
|
|
442
|
-
|
|
445
|
+
log.info(TAG, `Credential ${credId} failed (${classified.reason}: ${errMessage?.slice(0, 80)}), trying fallback...`)
|
|
443
446
|
opts.write(`data: ${JSON.stringify({
|
|
444
447
|
t: 'md',
|
|
445
448
|
text: JSON.stringify({ failover: { from: credId, reason: errMessage?.slice(0, 100) } }),
|
|
@@ -3,9 +3,12 @@ import http from 'http'
|
|
|
3
3
|
import https from 'https'
|
|
4
4
|
import type { StreamChatOptions } from './index'
|
|
5
5
|
import { IMAGE_EXTS, TEXT_EXTS, MAX_HISTORY_MESSAGES, writeSSE } from './provider-defaults'
|
|
6
|
+
import { log } from '@/lib/server/logger'
|
|
6
7
|
import { resolveOllamaRuntimeConfig } from '@/lib/server/ollama-runtime'
|
|
7
8
|
import { resolveImagePath } from '@/lib/server/resolve-image'
|
|
8
9
|
|
|
10
|
+
const TAG = 'provider-ollama'
|
|
11
|
+
|
|
9
12
|
export function streamOllamaChat({ session, message, imagePath, apiKey, write, active, loadHistory, onUsage, signal }: StreamChatOptions): Promise<string> {
|
|
10
13
|
return new Promise((resolve, reject) => {
|
|
11
14
|
const messages = buildMessages(session, message, imagePath, loadHistory)
|
|
@@ -69,7 +72,7 @@ export function streamOllamaChat({ session, message, imagePath, apiKey, write, a
|
|
|
69
72
|
apiRes.on('data', (c: Buffer) => errBody += c)
|
|
70
73
|
apiRes.on('end', () => {
|
|
71
74
|
const msg = `Ollama error ${apiRes.statusCode}: ${errBody.slice(0, 200)}`
|
|
72
|
-
|
|
75
|
+
log.error(TAG, `[${session.id}] ${msg}`)
|
|
73
76
|
writeSSE(write, 'err', msg.slice(0, 120))
|
|
74
77
|
active.delete(session.id)
|
|
75
78
|
reject(new Error(msg))
|
|
@@ -114,7 +117,7 @@ export function streamOllamaChat({ session, message, imagePath, apiKey, write, a
|
|
|
114
117
|
active.set(session.id, { kill: () => { abortController.aborted = true; apiReq.destroy() } })
|
|
115
118
|
|
|
116
119
|
apiReq.on('error', (e: NodeJS.ErrnoException) => {
|
|
117
|
-
|
|
120
|
+
log.error(TAG, `[${session.id}] ollama request error:`, e.message)
|
|
118
121
|
let errMsg = e.message
|
|
119
122
|
if (e.code === 'ECONNREFUSED') {
|
|
120
123
|
errMsg = `Cannot connect to Ollama at ${endpoint}. Is Ollama running?`
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import fs from 'fs'
|
|
2
2
|
import type { StreamChatOptions } from './index'
|
|
3
3
|
import { PROVIDER_DEFAULTS, IMAGE_EXTS, TEXT_EXTS, PDF_MAX_CHARS, MAX_HISTORY_MESSAGES, writeSSE } from './provider-defaults'
|
|
4
|
+
import { log } from '@/lib/server/logger'
|
|
4
5
|
import { resolveImagePath } from '@/lib/server/resolve-image'
|
|
5
6
|
|
|
7
|
+
const TAG = 'provider-openai'
|
|
8
|
+
|
|
6
9
|
async function fileToContentParts(filePath: string): Promise<Array<Record<string, unknown>>> {
|
|
7
10
|
if (!filePath || !fs.existsSync(filePath)) return []
|
|
8
11
|
const name = filePath.split('/').pop() || 'file'
|
|
@@ -108,7 +111,7 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
108
111
|
const resContentType = res.headers.get('content-type') || ''
|
|
109
112
|
if (resContentType.includes('text/html')) {
|
|
110
113
|
const msg = 'Received HTML instead of API response. The endpoint may be misconfigured or returning a landing page.'
|
|
111
|
-
|
|
114
|
+
log.error(TAG, `[${session.id}] received HTML instead of API response from ${baseUrl} (provider: ${session.provider})`)
|
|
112
115
|
writeSSE(write, 'err', msg)
|
|
113
116
|
active.delete(session.id)
|
|
114
117
|
reject(new Error(msg))
|
|
@@ -117,7 +120,7 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
117
120
|
|
|
118
121
|
if (!res.ok) {
|
|
119
122
|
const errBody = await res.text().catch(() => '')
|
|
120
|
-
|
|
123
|
+
log.error(TAG, `[${session.id}] openai error ${res.status}:`, errBody.slice(0, 200))
|
|
121
124
|
let errMsg = `API error (${res.status})`
|
|
122
125
|
try {
|
|
123
126
|
const parsed = JSON.parse(errBody)
|
|
@@ -133,7 +136,7 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
133
136
|
|
|
134
137
|
if (!res.body) {
|
|
135
138
|
const msg = `No response body from ${baseUrl}`
|
|
136
|
-
|
|
139
|
+
log.error(TAG, `[${session.id}] ${msg}`)
|
|
137
140
|
active.delete(session.id)
|
|
138
141
|
reject(new Error(msg))
|
|
139
142
|
return
|
|
@@ -175,12 +178,12 @@ export function streamOpenAiChat({ session, message, imagePath, imageUrl, apiKey
|
|
|
175
178
|
}
|
|
176
179
|
|
|
177
180
|
if (!fullResponse) {
|
|
178
|
-
|
|
181
|
+
log.error(TAG, `[${session.id}] openai stream ended with no content (provider: ${session.provider}, endpoint: ${baseUrl})`)
|
|
179
182
|
}
|
|
180
183
|
} catch (err: unknown) {
|
|
181
184
|
const errObj = err as { name?: string; message?: string }
|
|
182
185
|
if (errObj.name !== 'AbortError') {
|
|
183
|
-
|
|
186
|
+
log.error(TAG, `[${session.id}] openai request error:`, errObj.message)
|
|
184
187
|
writeSSE(write, 'err', `Connection failed: ${errObj.message}`)
|
|
185
188
|
}
|
|
186
189
|
active.delete(session.id)
|
|
@@ -2,7 +2,7 @@ import { spawn } from 'child_process'
|
|
|
2
2
|
import type { StreamChatOptions } from './index'
|
|
3
3
|
import { log } from '../server/logger'
|
|
4
4
|
import { loadRuntimeSettings } from '@/lib/server/runtime/runtime-settings'
|
|
5
|
-
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler } from './cli-utils'
|
|
5
|
+
import { resolveCliBinary, buildCliEnv, probeCliAuth, attachAbortHandler, isStderrNoise } from './cli-utils'
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* OpenCode CLI provider — spawns `opencode run <message> --format json` for non-interactive usage.
|
|
@@ -120,7 +120,11 @@ export function streamOpenCodeCliChat({ session, message, imagePath, systemPromp
|
|
|
120
120
|
const text = chunk.toString()
|
|
121
121
|
stderrText += text
|
|
122
122
|
if (stderrText.length > 16_000) stderrText = stderrText.slice(-16_000)
|
|
123
|
-
|
|
123
|
+
if (isStderrNoise(text)) {
|
|
124
|
+
log.debug('opencode-cli', `stderr noise [${session.id}]`, text.slice(0, 500))
|
|
125
|
+
} else {
|
|
126
|
+
log.warn('opencode-cli', `stderr [${session.id}]`, text.slice(0, 500))
|
|
127
|
+
}
|
|
124
128
|
})
|
|
125
129
|
|
|
126
130
|
return new Promise((resolve) => {
|
|
@@ -52,19 +52,36 @@ export function getAgentDirectory(excludeId?: string): AgentDirectoryEntry[] {
|
|
|
52
52
|
return entries
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export function buildAgentAwarenessBlock(
|
|
56
|
-
|
|
55
|
+
export function buildAgentAwarenessBlock(
|
|
56
|
+
excludeId: string,
|
|
57
|
+
opts?: {
|
|
58
|
+
delegationTargetMode?: 'all' | 'selected'
|
|
59
|
+
delegationTargetAgentIds?: string[]
|
|
60
|
+
},
|
|
61
|
+
): string {
|
|
62
|
+
let directory = getAgentDirectory(excludeId)
|
|
57
63
|
if (!directory.length) return ''
|
|
58
64
|
|
|
65
|
+
const isFiltered = opts?.delegationTargetMode === 'selected'
|
|
66
|
+
if (isFiltered) {
|
|
67
|
+
const allowedIds = new Set(opts.delegationTargetAgentIds || [])
|
|
68
|
+
directory = directory.filter((entry) => allowedIds.has(entry.id))
|
|
69
|
+
if (!directory.length) return ''
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
const lines = directory.map((entry) => {
|
|
60
73
|
const caps = entry.capabilities.length ? ` (${entry.capabilities.join(', ')})` : ''
|
|
61
74
|
const status = entry.statusDetail || entry.status
|
|
62
75
|
return `- **${entry.name}** [id: ${entry.id}]${caps} — ${status}`
|
|
63
76
|
})
|
|
64
77
|
|
|
78
|
+
const header = isFiltered
|
|
79
|
+
? 'These are the ONLY agents I can delegate tasks to. Do not attempt to delegate to any other agents:'
|
|
80
|
+
: 'These are the other agents I work alongside. I can hand off tasks to any of them if their skills are a better fit:'
|
|
81
|
+
|
|
65
82
|
return [
|
|
66
83
|
'## My Colleagues',
|
|
67
|
-
|
|
84
|
+
header,
|
|
68
85
|
...lines,
|
|
69
86
|
].join('\n')
|
|
70
87
|
}
|
|
@@ -12,7 +12,7 @@ const MAX_PENDING_EVENTS = 16
|
|
|
12
12
|
const MAX_TIMELINE_ITEMS = 40
|
|
13
13
|
const MAX_WORKING_MEMORY_NOTES = 12
|
|
14
14
|
const DEFAULT_FOLLOWUP_DELAY_MS = 1500
|
|
15
|
-
const DEFAULT_MAX_FOLLOWUP_CHAIN =
|
|
15
|
+
const DEFAULT_MAX_FOLLOWUP_CHAIN = 4
|
|
16
16
|
const MAX_LIFETIME_ITERATIONS = 200
|
|
17
17
|
|
|
18
18
|
export interface MainLoopState {
|
|
@@ -778,6 +778,7 @@ export function buildMainLoopHeartbeatPrompt(session: unknown, fallbackPrompt: s
|
|
|
778
778
|
boundedFallbackPrompt ? `Base heartbeat instructions:\n${boundedFallbackPrompt}` : '',
|
|
779
779
|
'',
|
|
780
780
|
'You are checking the durable main mission thread for this agent.',
|
|
781
|
+
'Keep this status check brief — 5-10 tool calls maximum. Read key state, summarize progress, and report. Do not attempt fixes or deep investigation during heartbeats.',
|
|
781
782
|
'Use only the current goal, plan, next action, and pending external events shown above.',
|
|
782
783
|
'Do not infer or repeat old tasks from prior heartbeats.',
|
|
783
784
|
'Prefer taking the single highest-value next step over restating the plan. Do not repeat completed work.',
|
|
@@ -1072,8 +1073,8 @@ export function handleMainLoopRunResult(input: HandleMainLoopRunResultInput): Ma
|
|
|
1072
1073
|
|| (needsReplan
|
|
1073
1074
|
? 'Replan from the latest outcome, then execute only the highest-value remaining step. Do not repeat completed work.'
|
|
1074
1075
|
: state.nextAction
|
|
1075
|
-
? `Continue
|
|
1076
|
-
:
|
|
1076
|
+
? `Continue. Next action: ${state.nextAction}. Do not repeat tool calls from previous turns.`
|
|
1077
|
+
: `Continue. You have used ${state.followupChainCount} of ${limit} followup turns. Focus on completing one concrete step, then summarize progress.`)
|
|
1077
1078
|
followup = {
|
|
1078
1079
|
message,
|
|
1079
1080
|
delayMs: DEFAULT_FOLLOWUP_DELAY_MS,
|
|
@@ -34,6 +34,8 @@ import { logExecution } from '@/lib/server/execution-log'
|
|
|
34
34
|
import { logActivity } from '@/lib/server/storage'
|
|
35
35
|
import { createNotification } from '@/lib/server/create-notification'
|
|
36
36
|
|
|
37
|
+
const TAG = 'supervisor-reflection'
|
|
38
|
+
|
|
37
39
|
const MAIN_LOOP_META_LINE_RE = /\[(?:MAIN_LOOP_META|MAIN_LOOP_PLAN|MAIN_LOOP_REVIEW|AGENT_HEARTBEAT_META)\]\s*(\{[^\n]*\})?/i
|
|
38
40
|
const DEFAULT_TRANSCRIPT_MESSAGES = 12
|
|
39
41
|
const DEFAULT_SNIPPET_CHARS = 800
|
|
@@ -368,6 +370,17 @@ export function assessAutonomyRun(input: {
|
|
|
368
370
|
if (strongest?.kind === 'budget_pressure' && strongest.severity === 'high') shouldBlock = true
|
|
369
371
|
if (strongest?.kind === 'run_error' && (status === 'failed' || status === 'cancelled')) shouldBlock = true
|
|
370
372
|
|
|
373
|
+
// Block after 3+ no_progress or repeated_tool incidents within 30 minutes for the same session
|
|
374
|
+
if (!shouldBlock && strongest && (strongest.kind === 'no_progress' || strongest.kind === 'repeated_tool')) {
|
|
375
|
+
const existingIncidents = Object.values(loadSupervisorIncidents()) as SupervisorIncident[]
|
|
376
|
+
const recentSame = existingIncidents.filter((i) =>
|
|
377
|
+
i.sessionId === input.sessionId
|
|
378
|
+
&& i.createdAt > Date.now() - 30 * 60_000
|
|
379
|
+
&& (i.kind === 'no_progress' || i.kind === 'repeated_tool'),
|
|
380
|
+
)
|
|
381
|
+
if (recentSame.length >= 3) shouldBlock = true
|
|
382
|
+
}
|
|
383
|
+
|
|
371
384
|
const seen = new Set<string>()
|
|
372
385
|
const autoActions: AutonomyAssessment['autoActions'] = []
|
|
373
386
|
for (const incident of incidents) {
|
|
@@ -1023,7 +1036,7 @@ export async function observeAutonomyRunOutcome(
|
|
|
1023
1036
|
try {
|
|
1024
1037
|
parsed = parseReflectionResponse(responseText)
|
|
1025
1038
|
} catch {
|
|
1026
|
-
|
|
1039
|
+
log.warn(TAG, `Reflection parse failed for run ${input.runId}, skipping reflection`)
|
|
1027
1040
|
return { incidents, reflection: null }
|
|
1028
1041
|
}
|
|
1029
1042
|
if (parsed.skip) return { incidents, reflection: null }
|
|
@@ -109,6 +109,8 @@ import {
|
|
|
109
109
|
resolveMissionForTurn,
|
|
110
110
|
} from '@/lib/server/missions/mission-service'
|
|
111
111
|
|
|
112
|
+
const TAG = 'chat-execution'
|
|
113
|
+
|
|
112
114
|
export {
|
|
113
115
|
shouldApplySessionFreshnessReset,
|
|
114
116
|
shouldAutoRouteHeartbeatAlerts,
|
|
@@ -1218,6 +1220,15 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1218
1220
|
const emit = (ev: SSEEvent) => {
|
|
1219
1221
|
let shouldPersistPartial = false
|
|
1220
1222
|
let immediatePartialPersist = false
|
|
1223
|
+
if (ev.t === 'reset') {
|
|
1224
|
+
// stream-agent-chat rolls back state after a transient error — reset
|
|
1225
|
+
// accumulated text/thinking/tools so the partial persist stays in sync.
|
|
1226
|
+
streamingPartialText = ev.text || ''
|
|
1227
|
+
thinkingText = ''
|
|
1228
|
+
toolEvents.length = 0
|
|
1229
|
+
shouldPersistPartial = true
|
|
1230
|
+
immediatePartialPersist = true
|
|
1231
|
+
}
|
|
1221
1232
|
if (ev.t === 'd' && typeof ev.text === 'string') {
|
|
1222
1233
|
streamingPartialText += ev.text
|
|
1223
1234
|
shouldPersistPartial = true
|
|
@@ -1378,7 +1389,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1378
1389
|
? (heartbeatLightContext ? [] : getSessionMessages(sessionId).slice(-6))
|
|
1379
1390
|
: undefined
|
|
1380
1391
|
|
|
1381
|
-
|
|
1392
|
+
log.info(TAG, `provider=${providerType}, hasExtensions=${hasExtensions}, localOpenClawNative=${useLocalOpenClawNativeRuntime}, imagePath=${resolvedImagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, extensions=${enabledSessionExtensions.length}`)
|
|
1382
1393
|
if (hasExtensions) {
|
|
1383
1394
|
const result = await streamAgentChat({
|
|
1384
1395
|
session: sessionForRun,
|
|
@@ -1392,6 +1403,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1392
1403
|
write: (raw) => parseAndEmit(raw),
|
|
1393
1404
|
history: heartbeatHistory ?? applyContextClearBoundary(getSessionMessages(sessionId)),
|
|
1394
1405
|
signal: abortController.signal,
|
|
1406
|
+
source,
|
|
1395
1407
|
})
|
|
1396
1408
|
fullResponse = result.finalResponse || result.fullText
|
|
1397
1409
|
} else {
|
|
@@ -1760,7 +1772,7 @@ export async function executeSessionChatTurn(input: ExecuteChatTurnInput): Promi
|
|
|
1760
1772
|
now: nowTs,
|
|
1761
1773
|
})) {
|
|
1762
1774
|
persistedResponseForHooks = nextAssistantMessage.text
|
|
1763
|
-
} else if (
|
|
1775
|
+
} else if (previous?.runId === lifecycleRunId || shouldReplaceRecentAssistantMessage({
|
|
1764
1776
|
previous,
|
|
1765
1777
|
nextToolEvents,
|
|
1766
1778
|
nextKind,
|
|
@@ -143,9 +143,10 @@ function checkLoopDetection(ctx: ContinuationContext): ContinuationDecision | nu
|
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
// Terminal — caller should break
|
|
147
|
-
|
|
148
|
-
|
|
146
|
+
// Terminal — caller should break.
|
|
147
|
+
// Emit a user-friendly message instead of the raw diagnostic (which is internal).
|
|
148
|
+
// The structured diagnostic data is already carried via the `status` event in iteration-event-handler.
|
|
149
|
+
ctx.write(`data: ${JSON.stringify({ t: 'err', text: 'The agent got stuck in a repetitive loop and has been stopped. Please try rephrasing your request or breaking it into smaller steps.' })}\n\n`)
|
|
149
150
|
return { type: false, requiredToolReminderNames: [] }
|
|
150
151
|
}
|
|
151
152
|
|
|
@@ -54,7 +54,7 @@ const MAX_COORDINATOR_SYNTHESIS = 3
|
|
|
54
54
|
const MAX_COORDINATOR_DELEGATION_NUDGE = 1
|
|
55
55
|
|
|
56
56
|
/** Max loop recovery continuations (tool_frequency limit resets) */
|
|
57
|
-
const MAX_LOOP_RECOVERY =
|
|
57
|
+
const MAX_LOOP_RECOVERY = 1
|
|
58
58
|
|
|
59
59
|
/** Max context overflow retries (emergency context reduction) */
|
|
60
60
|
const MAX_CONTEXT_OVERFLOW = 2
|
|
@@ -64,7 +64,7 @@ const MAX_CONTEXT_OVERFLOW = 2
|
|
|
64
64
|
export class ContinuationLimits {
|
|
65
65
|
private readonly limits: Record<BudgetedContinuation, LimitEntry>
|
|
66
66
|
|
|
67
|
-
constructor(isConnectorSession: boolean) {
|
|
67
|
+
constructor(isConnectorSession: boolean, isHeartbeat = false) {
|
|
68
68
|
let maxDeliverableFollowthroughs = MAX_DELIVERABLE_FOLLOWTHROUGH
|
|
69
69
|
let maxExecutionFollowthroughs = MAX_EXECUTION_FOLLOWTHROUGH
|
|
70
70
|
let maxAttachmentFollowthroughs = MAX_ATTACHMENT_FOLLOWTHROUGH
|
|
@@ -79,6 +79,9 @@ export class ContinuationLimits {
|
|
|
79
79
|
maxUnfinishedToolFollowthroughs = 1
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// Heartbeats should not need loop recovery — they are brief status checks
|
|
83
|
+
const maxLoopRecovery = isHeartbeat ? 0 : MAX_LOOP_RECOVERY
|
|
84
|
+
|
|
82
85
|
this.limits = {
|
|
83
86
|
recursion: { count: 0, max: MAX_RECURSION },
|
|
84
87
|
transient: { count: 0, max: MAX_TRANSIENT },
|
|
@@ -94,7 +97,7 @@ export class ContinuationLimits {
|
|
|
94
97
|
tool_summary: { count: 0, max: maxToolSummaryRetries },
|
|
95
98
|
coordinator_synthesis: { count: 0, max: MAX_COORDINATOR_SYNTHESIS },
|
|
96
99
|
coordinator_delegation_nudge: { count: 0, max: MAX_COORDINATOR_DELEGATION_NUDGE },
|
|
97
|
-
loop_recovery: { count: 0, max:
|
|
100
|
+
loop_recovery: { count: 0, max: maxLoopRecovery },
|
|
98
101
|
}
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -13,9 +13,12 @@ import crypto from 'node:crypto'
|
|
|
13
13
|
import { HumanMessage } from '@langchain/core/messages'
|
|
14
14
|
import { z } from 'zod'
|
|
15
15
|
import { buildLLM } from '@/lib/server/build-llm'
|
|
16
|
+
import { log } from '@/lib/server/logger'
|
|
16
17
|
import { hmrSingleton } from '@/lib/shared-utils'
|
|
17
18
|
import type { Message } from '@/types'
|
|
18
19
|
|
|
20
|
+
const TAG = 'message-classifier'
|
|
21
|
+
|
|
19
22
|
// ---------------------------------------------------------------------------
|
|
20
23
|
// Schema
|
|
21
24
|
// ---------------------------------------------------------------------------
|
|
@@ -221,7 +224,7 @@ export async function classifyMessage(
|
|
|
221
224
|
])
|
|
222
225
|
|
|
223
226
|
const durationMs = Date.now() - startMs
|
|
224
|
-
|
|
227
|
+
log.info(TAG, `session=${input.sessionId} completed in ${durationMs}ms`)
|
|
225
228
|
|
|
226
229
|
const classification = parseClassificationResponse(responseText)
|
|
227
230
|
if (classification) {
|
|
@@ -230,7 +233,7 @@ export async function classifyMessage(
|
|
|
230
233
|
return classification
|
|
231
234
|
} catch (err: unknown) {
|
|
232
235
|
const durationMs = Date.now() - startMs
|
|
233
|
-
|
|
236
|
+
log.warn(TAG, `session=${input.sessionId} failed in ${durationMs}ms: ${err instanceof Error ? err.message : 'unknown'}`)
|
|
234
237
|
return null
|
|
235
238
|
}
|
|
236
239
|
}
|
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* and OpenClaw sync.
|
|
7
7
|
*/
|
|
8
8
|
import type { Session, UsageRecord } from '@/types'
|
|
9
|
+
import { log } from '@/lib/server/logger'
|
|
9
10
|
import type { ChatTurnState } from '@/lib/server/chat-execution/chat-turn-state'
|
|
11
|
+
|
|
12
|
+
const TAG = 'post-stream'
|
|
10
13
|
import { extractSuggestions } from '@/lib/server/suggestions'
|
|
11
14
|
import type { StructuredToolInterface } from '@langchain/core/tools'
|
|
12
15
|
import { estimateCost, buildExtensionDefinitionCosts } from '@/lib/server/cost'
|
|
@@ -39,7 +42,7 @@ function stripLeakedClassificationJson(text: string): { cleaned: string; strippe
|
|
|
39
42
|
else if (text[i] === '}') { depth--; if (depth === 0) { end = i + 1; break } }
|
|
40
43
|
}
|
|
41
44
|
if (end === -1) return { cleaned: text, stripped: false }
|
|
42
|
-
|
|
45
|
+
log.warn(TAG, 'Stripped leaked classification JSON from model output')
|
|
43
46
|
return { cleaned: (text.slice(0, startIdx) + text.slice(end)).trimStart(), stripped: true }
|
|
44
47
|
}
|
|
45
48
|
|
|
@@ -120,6 +120,16 @@ export function buildToolDisciplineLines(enabledExtensions: string[]): string[]
|
|
|
120
120
|
|
|
121
121
|
lines.push(...planning.disciplineGuidance)
|
|
122
122
|
|
|
123
|
+
// Universal tool efficiency guidance — tool-specific lines live in CORE_TOOL_PLANNING (tool-planning.ts)
|
|
124
|
+
lines.push(
|
|
125
|
+
'## Tool Efficiency',
|
|
126
|
+
'Plan your approach before starting tool calls. State what you will do, then do it.',
|
|
127
|
+
'Prefer fewer, larger tool calls over many small ones.',
|
|
128
|
+
'Do not poll for status in a loop. If waiting on a process, check once and move on.',
|
|
129
|
+
'If stuck after 2-3 attempts with the same approach, stop and state the blocker — do not keep retrying.',
|
|
130
|
+
'When delegating to subagents, use waitForCompletion or wait/wait_all instead of polling status in a loop.',
|
|
131
|
+
)
|
|
132
|
+
|
|
123
133
|
const researchSearchTools = getToolsForCapability(enabledExtensions, TOOL_CAPABILITY.researchSearch)
|
|
124
134
|
const researchFetchTools = getToolsForCapability(enabledExtensions, TOOL_CAPABILITY.researchFetch)
|
|
125
135
|
const browserCaptureTools = getToolsForCapability(enabledExtensions, TOOL_CAPABILITY.browserCapture)
|
|
@@ -275,7 +285,7 @@ const GOAL_DECOMPOSITION_BLOCK = [
|
|
|
275
285
|
'## Goal Decomposition',
|
|
276
286
|
'When you receive a broad, open-ended goal:',
|
|
277
287
|
'1. Break it into 3-7 concrete, sequentially-executable subtasks before taking action.',
|
|
278
|
-
'2. If manage_tasks is available, use it only for durable tracking: multi-turn work, delegation, explicit backlog requests, or work you expect to resume later. Do not create a task for every micro-step.',
|
|
288
|
+
'2. If manage_tasks is available, use it only for durable tracking: multi-turn work, delegation, explicit backlog requests, or work you expect to resume later. Do not create a task for every micro-step. Do not re-read the task list after every update. Read once, make your changes, then move on.',
|
|
279
289
|
'Single-step instructions are not broad goals. For direct actions like storing a memory, answering a recall question, editing one file, or sending one message, execute the relevant tool immediately instead of creating tasks or delegating.',
|
|
280
290
|
'3. Present the plan as a short checklist or numbered list in plain language. If durable tracking is unnecessary, keep it inline instead of creating tasks.',
|
|
281
291
|
'4. Execute the first substantive subtask immediately — do not stop after planning.',
|
|
@@ -123,7 +123,21 @@ export async function buildAgentAwarenessSection(
|
|
|
123
123
|
if (!hasMultiAgentTool || !session.agentId) return null
|
|
124
124
|
try {
|
|
125
125
|
const { buildAgentAwarenessBlock } = await import('@/lib/server/agents/agent-registry')
|
|
126
|
-
|
|
126
|
+
|
|
127
|
+
// Load agent to get delegation settings so the awareness block respects them
|
|
128
|
+
let delegationOpts: { delegationTargetMode?: 'all' | 'selected'; delegationTargetAgentIds?: string[] } | undefined
|
|
129
|
+
try {
|
|
130
|
+
const agents = loadAgents() as Record<string, Agent>
|
|
131
|
+
const agent = agents[session.agentId]
|
|
132
|
+
if (agent?.delegationTargetMode === 'selected') {
|
|
133
|
+
delegationOpts = {
|
|
134
|
+
delegationTargetMode: 'selected',
|
|
135
|
+
delegationTargetAgentIds: agent.delegationTargetAgentIds || [],
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
} catch { /* non-critical */ }
|
|
139
|
+
|
|
140
|
+
return buildAgentAwarenessBlock(session.agentId, delegationOpts) || null
|
|
127
141
|
} catch { return null }
|
|
128
142
|
}
|
|
129
143
|
|
|
@@ -185,6 +199,8 @@ export function buildProjectSection(
|
|
|
185
199
|
`project secrets ${summary.secretCount}`,
|
|
186
200
|
]
|
|
187
201
|
if (summary.topTaskTitles.length > 0) lines.push(`Top open tasks: ${summary.topTaskTitles.join('; ')}`)
|
|
202
|
+
if (summary.failedTaskCount > 0) lines.push(`Failed tasks needing attention: ${summary.failedTaskCount}`)
|
|
203
|
+
if (summary.staleTaskCount > 0) lines.push(`Stale tasks (no update in 3+ days): ${summary.staleTaskCount}`)
|
|
188
204
|
if (summary.scheduleNames.length > 0) lines.push(`Active schedules: ${summary.scheduleNames.join('; ')}`)
|
|
189
205
|
if (summary.secretNames.length > 0) lines.push(`Known project secrets: ${summary.secretNames.join('; ')}`)
|
|
190
206
|
lines.push(`Project resource summary: ${resourceBits.join(', ')}.`)
|
|
@@ -257,6 +273,11 @@ export function buildSuggestionsSection(
|
|
|
257
273
|
// Proactive Memory Recall (async)
|
|
258
274
|
// ---------------------------------------------------------------------------
|
|
259
275
|
|
|
276
|
+
export interface ProactiveMemoryResult {
|
|
277
|
+
section: string | null
|
|
278
|
+
injectedIds: Record<string, number>
|
|
279
|
+
}
|
|
280
|
+
|
|
260
281
|
export async function buildProactiveMemorySection(
|
|
261
282
|
session: Session,
|
|
262
283
|
agent: Agent | null | undefined,
|
|
@@ -264,9 +285,10 @@ export async function buildProactiveMemorySection(
|
|
|
264
285
|
activeProjectRoot: string | null,
|
|
265
286
|
isMinimalPrompt: boolean,
|
|
266
287
|
currentThreadRecallRequest: boolean,
|
|
267
|
-
): Promise<
|
|
268
|
-
|
|
269
|
-
if (!
|
|
288
|
+
): Promise<ProactiveMemoryResult> {
|
|
289
|
+
const noResult: ProactiveMemoryResult = { section: null, injectedIds: {} }
|
|
290
|
+
if (isMinimalPrompt || !session.agentId || currentThreadRecallRequest || message.length <= 12) return noResult
|
|
291
|
+
if (!agent?.proactiveMemory) return noResult
|
|
270
292
|
try {
|
|
271
293
|
const { getMemoryDb } = await import('@/lib/server/memory/memory-db')
|
|
272
294
|
const { buildSessionMemoryScopeFilter } = await import('@/lib/server/memory/session-memory-scope')
|
|
@@ -274,13 +296,29 @@ export async function buildProactiveMemorySection(
|
|
|
274
296
|
const recalled = memDb.search(message, session.agentId, {
|
|
275
297
|
scope: buildSessionMemoryScopeFilter(session, agent.memoryScopeMode || null, activeProjectRoot),
|
|
276
298
|
})
|
|
277
|
-
|
|
299
|
+
|
|
300
|
+
// Dedup: skip memories already injected 2+ times in this session
|
|
301
|
+
const priorCounts = session.injectedMemoryIds || {}
|
|
302
|
+
const filtered = recalled.filter((entry) => (priorCounts[entry.id] || 0) < 2)
|
|
303
|
+
|
|
304
|
+
const topRecalled = filtered.slice(0, 3)
|
|
278
305
|
if (topRecalled.length > 0) {
|
|
279
|
-
|
|
280
|
-
|
|
306
|
+
// Track injection counts
|
|
307
|
+
const updatedCounts: Record<string, number> = { ...priorCounts }
|
|
308
|
+
for (const entry of topRecalled) {
|
|
309
|
+
updatedCounts[entry.id] = (updatedCounts[entry.id] || 0) + 1
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const recalledLines = topRecalled.map((entry) =>
|
|
313
|
+
`- ${entry.abstract || entry.content.slice(0, 300)}`,
|
|
314
|
+
)
|
|
315
|
+
return {
|
|
316
|
+
section: `## Recalled Context\nRelevant memories from previous interactions:\n${recalledLines.join('\n')}`,
|
|
317
|
+
injectedIds: updatedCounts,
|
|
318
|
+
}
|
|
281
319
|
}
|
|
282
320
|
} catch { /* non-critical */ }
|
|
283
|
-
return
|
|
321
|
+
return noResult
|
|
284
322
|
}
|
|
285
323
|
|
|
286
324
|
// ---------------------------------------------------------------------------
|
|
@@ -328,7 +366,7 @@ export function buildCoordinatorSection(
|
|
|
328
366
|
for (const w of listed) {
|
|
329
367
|
const caps = w.capabilities?.length ? ` [${w.capabilities.join(', ')}]` : ''
|
|
330
368
|
const desc = w.description ? ` — ${w.description.slice(0, 100)}` : ''
|
|
331
|
-
const line = `- **${w.name}
|
|
369
|
+
const line = `- **${w.name}** [id: ${w.id}]${caps}${desc}`
|
|
332
370
|
if (charBudget - line.length < 0) break
|
|
333
371
|
charBudget -= line.length + 1
|
|
334
372
|
lines.push(line)
|
|
@@ -338,6 +376,11 @@ export function buildCoordinatorSection(
|
|
|
338
376
|
lines.push(`- ... and ${workers.length - COORDINATOR_MAX_WORKERS} more workers`)
|
|
339
377
|
}
|
|
340
378
|
|
|
379
|
+
if (delegateMode === 'selected') {
|
|
380
|
+
lines.push('')
|
|
381
|
+
lines.push('**IMPORTANT:** You may ONLY delegate to the workers listed above. Do NOT attempt to delegate to any other agents — such attempts will be rejected.')
|
|
382
|
+
}
|
|
383
|
+
|
|
341
384
|
lines.push('')
|
|
342
385
|
lines.push('### Orchestration Tools')
|
|
343
386
|
lines.push('- **`spawn_subagent`** — Simple fire-and-forget delegation. Best for: single tasks, batch parallel/serial, basic swarm. Use when tasks are independent.')
|
|
@@ -9,6 +9,9 @@ import crypto from 'node:crypto'
|
|
|
9
9
|
import { HumanMessage } from '@langchain/core/messages'
|
|
10
10
|
import { z } from 'zod'
|
|
11
11
|
import { buildLLM } from '@/lib/server/build-llm'
|
|
12
|
+
import { log } from '@/lib/server/logger'
|
|
13
|
+
|
|
14
|
+
const TAG = 'response-completeness'
|
|
12
15
|
|
|
13
16
|
// ---------------------------------------------------------------------------
|
|
14
17
|
// Schema
|
|
@@ -191,7 +194,7 @@ export async function evaluateResponseCompleteness(
|
|
|
191
194
|
])
|
|
192
195
|
|
|
193
196
|
const durationMs = Date.now() - startMs
|
|
194
|
-
|
|
197
|
+
log.info(TAG, `session=${input.sessionId} completed in ${durationMs}ms`)
|
|
195
198
|
|
|
196
199
|
const completeness = parseCompletenessResponse(responseText)
|
|
197
200
|
if (completeness) {
|
|
@@ -200,7 +203,7 @@ export async function evaluateResponseCompleteness(
|
|
|
200
203
|
return completeness
|
|
201
204
|
} catch (err: unknown) {
|
|
202
205
|
const durationMs = Date.now() - startMs
|
|
203
|
-
|
|
206
|
+
log.warn(TAG, `session=${input.sessionId} failed in ${durationMs}ms: ${err instanceof Error ? err.message : 'unknown'}`)
|
|
204
207
|
return null
|
|
205
208
|
}
|
|
206
209
|
}
|