@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.
Files changed (123) hide show
  1. package/README.md +10 -0
  2. package/package.json +4 -1
  3. package/src/app/api/chats/[id]/deploy/route.ts +11 -6
  4. package/src/app/api/chats/[id]/devserver/route.ts +5 -2
  5. package/src/app/api/chats/[id]/messages/route.ts +7 -1
  6. package/src/app/api/credentials/[id]/route.ts +4 -1
  7. package/src/app/api/extensions/marketplace/route.ts +5 -2
  8. package/src/app/api/memory/maintenance/route.ts +5 -2
  9. package/src/app/api/preview-server/route.ts +14 -11
  10. package/src/app/api/system/status/route.ts +11 -0
  11. package/src/app/api/upload/route.ts +4 -1
  12. package/src/cli/index.js +7 -0
  13. package/src/cli/spec.js +1 -0
  14. package/src/components/agents/agent-files-editor.tsx +44 -32
  15. package/src/components/agents/personality-builder.tsx +13 -7
  16. package/src/components/agents/trash-list.tsx +1 -1
  17. package/src/components/chat/message-bubble.tsx +1 -0
  18. package/src/components/chat/message-list.tsx +25 -39
  19. package/src/components/chat/swarm-status-card.tsx +10 -3
  20. package/src/components/layout/daemon-indicator.tsx +7 -8
  21. package/src/components/layout/update-banner.tsx +8 -13
  22. package/src/components/logs/log-list.tsx +1 -1
  23. package/src/components/memory/memory-card.tsx +3 -1
  24. package/src/components/org-chart/org-chart-view.tsx +4 -0
  25. package/src/components/projects/project-list.tsx +4 -2
  26. package/src/components/projects/tabs/overview-tab.tsx +3 -2
  27. package/src/components/secrets/secret-sheet.tsx +1 -1
  28. package/src/components/secrets/secrets-list.tsx +1 -1
  29. package/src/components/shared/agent-switch-dialog.tsx +12 -6
  30. package/src/components/shared/dir-browser.tsx +22 -18
  31. package/src/components/skills/skill-sheet.tsx +2 -3
  32. package/src/components/tasks/task-list.tsx +1 -1
  33. package/src/components/tasks/task-sheet.tsx +1 -1
  34. package/src/hooks/use-openclaw-gateway.ts +46 -27
  35. package/src/instrumentation.ts +10 -7
  36. package/src/lib/chat/chat.ts +18 -2
  37. package/src/lib/providers/anthropic.ts +6 -3
  38. package/src/lib/providers/claude-cli.ts +9 -3
  39. package/src/lib/providers/cli-utils.ts +15 -0
  40. package/src/lib/providers/codex-cli.ts +9 -3
  41. package/src/lib/providers/gemini-cli.ts +6 -2
  42. package/src/lib/providers/index.ts +4 -1
  43. package/src/lib/providers/ollama.ts +5 -2
  44. package/src/lib/providers/openai.ts +8 -5
  45. package/src/lib/providers/opencode-cli.ts +6 -2
  46. package/src/lib/server/agents/agent-registry.ts +20 -3
  47. package/src/lib/server/agents/main-agent-loop.ts +4 -3
  48. package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
  49. package/src/lib/server/chat-execution/chat-execution.ts +14 -2
  50. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
  51. package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
  52. package/src/lib/server/chat-execution/message-classifier.ts +5 -2
  53. package/src/lib/server/chat-execution/post-stream-finalization.ts +4 -1
  54. package/src/lib/server/chat-execution/prompt-builder.ts +11 -1
  55. package/src/lib/server/chat-execution/prompt-sections.ts +52 -9
  56. package/src/lib/server/chat-execution/response-completeness.ts +5 -2
  57. package/src/lib/server/chat-execution/stream-agent-chat.ts +42 -12
  58. package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
  59. package/src/lib/server/connectors/bluebubbles.ts +7 -4
  60. package/src/lib/server/connectors/connector-inbound.ts +16 -13
  61. package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
  62. package/src/lib/server/connectors/connector-outbound.ts +6 -3
  63. package/src/lib/server/connectors/discord.ts +10 -7
  64. package/src/lib/server/connectors/email.ts +17 -14
  65. package/src/lib/server/connectors/googlechat.ts +7 -4
  66. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
  67. package/src/lib/server/connectors/matrix.ts +6 -3
  68. package/src/lib/server/connectors/openclaw.ts +20 -17
  69. package/src/lib/server/connectors/outbox.ts +4 -1
  70. package/src/lib/server/connectors/runtime-state.ts +19 -0
  71. package/src/lib/server/connectors/session-consolidation.ts +5 -2
  72. package/src/lib/server/connectors/signal.ts +9 -6
  73. package/src/lib/server/connectors/slack.ts +13 -10
  74. package/src/lib/server/connectors/teams.ts +8 -5
  75. package/src/lib/server/connectors/telegram.ts +15 -12
  76. package/src/lib/server/connectors/whatsapp.ts +32 -29
  77. package/src/lib/server/embeddings.ts +4 -1
  78. package/src/lib/server/link-understanding.ts +4 -1
  79. package/src/lib/server/memory/memory-abstract.ts +59 -0
  80. package/src/lib/server/memory/memory-db.ts +40 -14
  81. package/src/lib/server/missions/mission-service.ts +6 -3
  82. package/src/lib/server/openclaw/gateway.ts +8 -5
  83. package/src/lib/server/project-utils.ts +13 -0
  84. package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
  85. package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
  86. package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
  87. package/src/lib/server/provider-health.ts +18 -0
  88. package/src/lib/server/query-expansion.ts +4 -1
  89. package/src/lib/server/runtime/alert-dispatch.ts +7 -6
  90. package/src/lib/server/runtime/daemon-state.ts +189 -50
  91. package/src/lib/server/runtime/heartbeat-service.ts +23 -0
  92. package/src/lib/server/runtime/idle-window.ts +4 -1
  93. package/src/lib/server/runtime/perf.ts +4 -1
  94. package/src/lib/server/runtime/process-manager.ts +7 -4
  95. package/src/lib/server/runtime/queue.ts +31 -28
  96. package/src/lib/server/runtime/scheduler.ts +9 -6
  97. package/src/lib/server/runtime/session-run-manager.ts +3 -0
  98. package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
  99. package/src/lib/server/sandbox/novnc-auth.ts +10 -0
  100. package/src/lib/server/session-tools/context.ts +14 -0
  101. package/src/lib/server/session-tools/discovery.ts +9 -6
  102. package/src/lib/server/session-tools/index.ts +3 -1
  103. package/src/lib/server/session-tools/platform.ts +1 -1
  104. package/src/lib/server/session-tools/subagent.ts +23 -2
  105. package/src/lib/server/session-tools/wallet.ts +4 -1
  106. package/src/lib/server/skills/clawhub-client.ts +4 -1
  107. package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
  108. package/src/lib/server/skills/skill-eligibility.ts +6 -0
  109. package/src/lib/server/solana.ts +6 -0
  110. package/src/lib/server/storage-auth.ts +5 -5
  111. package/src/lib/server/storage-normalization.ts +4 -0
  112. package/src/lib/server/storage.ts +19 -8
  113. package/src/lib/server/tasks/task-followups.ts +4 -1
  114. package/src/lib/server/tool-loop-detection.ts +8 -3
  115. package/src/lib/server/tool-planning.ts +226 -0
  116. package/src/lib/server/tool-retry.ts +4 -3
  117. package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
  118. package/src/lib/server/ws-hub.ts +5 -2
  119. package/src/lib/strip-internal-metadata.test.ts +44 -4
  120. package/src/lib/strip-internal-metadata.ts +20 -6
  121. package/src/stores/use-approval-store.ts +7 -1
  122. package/src/stores/use-chat-store.ts +5 -1
  123. 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
- console.log(`[failover] Credential ${credId} failed (${classified.reason}: ${errMessage?.slice(0, 80)}), trying fallback...`)
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
- console.error(`[${session.id}] ${msg}`)
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
- console.error(`[${session.id}] ollama request error:`, e.message)
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
- console.error(`[${session.id}] received HTML instead of API response from ${baseUrl} (provider: ${session.provider})`)
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
- console.error(`[${session.id}] openai error ${res.status}:`, errBody.slice(0, 200))
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
- console.error(`[${session.id}] ${msg}`)
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
- console.error(`[${session.id}] openai stream ended with no content (provider: ${session.provider}, endpoint: ${baseUrl})`)
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
- console.error(`[${session.id}] openai request error:`, errObj.message)
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
- log.warn('opencode-cli', `stderr [${session.id}]`, text.slice(0, 500))
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(excludeId: string): string {
56
- const directory = getAgentDirectory(excludeId)
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
- 'These are the other agents I work alongside. I can hand off tasks to any of them if their skills are a better fit:',
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 = 6
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 the objective. Resume from this next action: ${state.nextAction}`
1076
- : 'Continue the objective and finish the next highest-value remaining step.')
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
- console.warn(`[autonomy] Reflection parse failed for run ${input.runId}, skipping reflection`)
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
- console.log(`[chat-execution] provider=${providerType}, hasExtensions=${hasExtensions}, localOpenClawNative=${useLocalOpenClawNativeRuntime}, imagePath=${resolvedImagePath || 'none'}, attachedFiles=${attachedFiles?.length || 0}, extensions=${enabledSessionExtensions.length}`)
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 ((previous?.streaming && previous?.runId === lifecycleRunId) || shouldReplaceRecentAssistantMessage({
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
- const errMessage = ctx.state.loopDetectionTriggered?.message || 'Tool frequency limit exceeded.'
148
- ctx.write(`data: ${JSON.stringify({ t: 'err', text: errMessage })}\n\n`) // err, not status
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 = 2
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: MAX_LOOP_RECOVERY },
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
- console.log(`[message-classifier] session=${input.sessionId} completed in ${durationMs}ms`)
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
- console.warn(`[message-classifier] session=${input.sessionId} failed in ${durationMs}ms: ${err instanceof Error ? err.message : 'unknown'}`)
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
- console.warn('[post-stream-finalization] Stripped leaked classification JSON from model output')
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
- return buildAgentAwarenessBlock(session.agentId) || null
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<string | null> {
268
- if (isMinimalPrompt || !session.agentId || currentThreadRecallRequest || message.length <= 12) return null
269
- if (!agent?.proactiveMemory) return null
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
- const topRecalled = recalled.slice(0, 3)
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
- const recalledLines = topRecalled.map((entry) => `- ${entry.content.slice(0, 300)}`)
280
- return `## Recalled Context\nRelevant memories from previous interactions:\n${recalledLines.join('\n')}`
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 null
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}**${caps}${desc}`
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
- console.log(`[response-completeness] session=${input.sessionId} completed in ${durationMs}ms`)
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
- console.warn(`[response-completeness] session=${input.sessionId} failed in ${durationMs}ms: ${err instanceof Error ? err.message : 'unknown'}`)
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
  }