@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
@@ -9,7 +9,7 @@ import type {
9
9
  SkillRequirements,
10
10
  SkillSecuritySummary,
11
11
  } from '@/types'
12
- import { dedup } from '@/lib/shared-utils'
12
+ import { dedup, hmrSingleton } from '@/lib/shared-utils'
13
13
  import { expandExtensionIds, getExtensionAliases, normalizeExtensionId } from '@/lib/server/tool-aliases'
14
14
  import { loadLearnedSkills, loadSettings, loadSkills } from '@/lib/server/storage'
15
15
  import { cosineSimilarity, getEmbedding } from '@/lib/server/embeddings'
@@ -160,7 +160,8 @@ const SOURCE_PRIORITY: Record<RuntimeSkillSource, number> = {
160
160
  }
161
161
 
162
162
  const DEFAULT_RUNTIME_SKILL_TOP_K = 8
163
- const embeddingCache = new Map<string, Promise<number[] | null>>()
163
+ const SKILL_EMBEDDING_CACHE_MAX = 200
164
+ const embeddingCache = hmrSingleton('__swarmclaw_skill_embedding_cache__', () => new Map<string, Promise<number[] | null>>())
164
165
 
165
166
  function normalizeKey(value: string | null | undefined): string {
166
167
  return String(value || '')
@@ -859,6 +860,11 @@ async function getCachedSkillEmbedding(
859
860
  const key = getSkillEmbeddingCacheKey(skill)
860
861
  let cached = embeddingCache.get(key)
861
862
  if (!cached) {
863
+ // FIFO eviction when cache exceeds cap
864
+ if (embeddingCache.size >= SKILL_EMBEDDING_CACHE_MAX) {
865
+ const firstKey = embeddingCache.keys().next().value
866
+ if (firstKey !== undefined) embeddingCache.delete(firstKey)
867
+ }
862
868
  cached = embeddingResolver(buildSkillEmbeddingText(skill))
863
869
  embeddingCache.set(key, cached)
864
870
  }
@@ -10,11 +10,17 @@ export interface SkillEligibilityResult {
10
10
  reasons: string[]
11
11
  }
12
12
 
13
+ const BINARY_CACHE_MAX = 200
13
14
  const binaryCache = new Map<string, boolean>()
14
15
 
15
16
  function hasBinary(name: string): boolean {
16
17
  const cached = binaryCache.get(name)
17
18
  if (cached !== undefined) return cached
19
+ // FIFO eviction at cap
20
+ if (binaryCache.size >= BINARY_CACHE_MAX) {
21
+ const firstKey = binaryCache.keys().next().value
22
+ if (firstKey !== undefined) binaryCache.delete(firstKey)
23
+ }
18
24
  try {
19
25
  execSync(`which ${name}`, { stdio: 'ignore', timeout: 2000 })
20
26
  binaryCache.set(name, true)
@@ -129,11 +129,17 @@ export function getSolanaExplorerUrl(cluster: SolanaCluster | string | null | un
129
129
  return `https://explorer.solana.com/${prefix}/${value}${clusterSuffix}`
130
130
  }
131
131
 
132
+ const CONNECTION_CACHE_MAX = 50
132
133
  const connectionCache = new Map<string, Connection>()
133
134
 
134
135
  function getCachedConnection(url: string): Connection {
135
136
  let conn = connectionCache.get(url)
136
137
  if (!conn) {
138
+ // FIFO eviction if cache exceeds cap
139
+ if (connectionCache.size >= CONNECTION_CACHE_MAX) {
140
+ const firstKey = connectionCache.keys().next().value
141
+ if (firstKey !== undefined) connectionCache.delete(firstKey)
142
+ }
137
143
  conn = new Connection(url, {
138
144
  commitment: 'confirmed',
139
145
  disableRetryOnRateLimit: true,
@@ -3,6 +3,9 @@ import path from 'path'
3
3
  import crypto from 'crypto'
4
4
 
5
5
  import { DATA_DIR, IS_BUILD_BOOTSTRAP } from './data-dir'
6
+ import { log } from '@/lib/server/logger'
7
+
8
+ const TAG = 'storage-auth'
6
9
 
7
10
  // --- .env loading ---
8
11
  function loadEnv() {
@@ -24,7 +27,7 @@ if (!IS_BUILD_BOOTSTRAP && !process.env.CREDENTIAL_SECRET) {
24
27
  const envPath = path.join(process.cwd(), '.env.local')
25
28
  fs.appendFileSync(envPath, `\nCREDENTIAL_SECRET=${secret}\n`)
26
29
  process.env.CREDENTIAL_SECRET = secret
27
- console.log('[credentials] Generated CREDENTIAL_SECRET in .env.local')
30
+ log.info(TAG, 'Generated CREDENTIAL_SECRET in .env.local')
28
31
  }
29
32
 
30
33
  // Auto-generate ACCESS_KEY if missing (used for simple auth)
@@ -35,10 +38,7 @@ if (!IS_BUILD_BOOTSTRAP && !process.env.ACCESS_KEY) {
35
38
  fs.appendFileSync(envPath, `\nACCESS_KEY=${key}\n`)
36
39
  process.env.ACCESS_KEY = key
37
40
  fs.writeFileSync(SETUP_FLAG, key)
38
- console.log(`\n${'='.repeat(50)}`)
39
- console.log(` ACCESS KEY: ${key}`)
40
- console.log(` Use this key to connect from the browser.`)
41
- console.log(`${'='.repeat(50)}\n`)
41
+ log.info(TAG, `ACCESS KEY: ${key} — Use this key to connect from the browser.`)
42
42
  }
43
43
 
44
44
  export function getAccessKey(): string {
@@ -526,5 +526,9 @@ function normalizeStoredRecordInner(
526
526
  if ('missionSummary' in session) delete session.missionSummary
527
527
  // Default geminiSessionId for new field
528
528
  if (session.geminiSessionId === undefined) session.geminiSessionId = null
529
+ // Default injectedMemoryIds for proactive recall dedup
530
+ if (!session.injectedMemoryIds || typeof session.injectedMemoryIds !== 'object') {
531
+ session.injectedMemoryIds = {}
532
+ }
529
533
  return session
530
534
  }
@@ -6,7 +6,10 @@ import type { ChildProcess } from 'node:child_process'
6
6
  import Database from 'better-sqlite3'
7
7
 
8
8
  import { perf } from '@/lib/server/runtime/perf'
9
+ import { log } from '@/lib/server/logger'
9
10
  import { notify } from '@/lib/server/ws-hub'
11
+
12
+ const TAG = 'storage'
10
13
  import { DATA_DIR, IS_BUILD_BOOTSTRAP, WORKSPACE_DIR } from './data-dir'
11
14
  import { normalizeHeartbeatSettingFields } from '@/lib/runtime/heartbeat-defaults'
12
15
  import { normalizeRuntimeSettingFields } from '@/lib/runtime/runtime-loop'
@@ -264,8 +267,8 @@ function saveCollection(table: string, data: Record<string, unknown>) {
264
267
  // partial collection instead of a full load-modify-save. This prevents
265
268
  // accidental data wipes (e.g. tests calling saveCredentials with 1 item).
266
269
  if (toDelete.length > 0 && next.size > 0 && toDelete.length > next.size) {
267
- console.error(
268
- `[storage] BLOCKED destructive saveCollection("${table}"): ` +
270
+ log.error(TAG,
271
+ `BLOCKED destructive saveCollection("${table}"): ` +
269
272
  `would delete ${toDelete.length} rows but only upsert ${next.size}. ` +
270
273
  `Use deleteCollectionItem() for explicit deletes or load-modify-save to update.`,
271
274
  )
@@ -537,7 +540,7 @@ const MIGRATION_FLAG = path.join(DATA_DIR, '.sqlite_migrated')
537
540
  function migrateFromJson() {
538
541
  if (fs.existsSync(MIGRATION_FLAG)) return
539
542
 
540
- console.log('[storage] Migrating from JSON files to SQLite...')
543
+ log.info(TAG, 'Migrating from JSON files to SQLite...')
541
544
 
542
545
  const transaction = db.transaction(() => {
543
546
  for (const [table, jsonPath] of Object.entries(JSON_FILES)) {
@@ -549,7 +552,7 @@ function migrateFromJson() {
549
552
  for (const [id, val] of Object.entries(data)) {
550
553
  ins.run(id, JSON.stringify(val))
551
554
  }
552
- console.log(`[storage] Migrated ${table}: ${Object.keys(data).length} records`)
555
+ log.info(TAG, `Migrated ${table}: ${Object.keys(data).length} records`)
553
556
  }
554
557
  } catch { /* skip malformed files */ }
555
558
  }
@@ -562,7 +565,7 @@ function migrateFromJson() {
562
565
  const data = JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
563
566
  if (data && Object.keys(data).length > 0) {
564
567
  saveSingleton('settings', data)
565
- console.log('[storage] Migrated settings')
568
+ log.info(TAG, 'Migrated settings')
566
569
  }
567
570
  } catch { /* skip */ }
568
571
  }
@@ -574,7 +577,7 @@ function migrateFromJson() {
574
577
  const data = JSON.parse(fs.readFileSync(queuePath, 'utf8'))
575
578
  if (Array.isArray(data) && data.length > 0) {
576
579
  saveSingleton('queue', data)
577
- console.log(`[storage] Migrated queue: ${data.length} items`)
580
+ log.info(TAG, `Migrated queue: ${data.length} items`)
578
581
  }
579
582
  } catch { /* skip */ }
580
583
  }
@@ -592,14 +595,14 @@ function migrateFromJson() {
592
595
  }
593
596
  }
594
597
  }
595
- console.log('[storage] Migrated usage records')
598
+ log.info(TAG, 'Migrated usage records')
596
599
  } catch { /* skip */ }
597
600
  }
598
601
  })
599
602
 
600
603
  transaction()
601
604
  fs.writeFileSync(MIGRATION_FLAG, new Date().toISOString())
602
- console.log('[storage] Migration complete. JSON files preserved as backup.')
605
+ log.info(TAG, 'Migration complete. JSON files preserved as backup.')
603
606
  }
604
607
 
605
608
  if (!IS_BUILD_BOOTSTRAP) {
@@ -1400,6 +1403,14 @@ export function appendUsage(sessionId: string, record: unknown) {
1400
1403
  ins.run(sessionId, JSON.stringify(record))
1401
1404
  }
1402
1405
 
1406
+ export function pruneOldUsage(maxAgeMs: number): number {
1407
+ const cutoff = Date.now() - maxAgeMs
1408
+ const result = db.prepare(
1409
+ `DELETE FROM usage WHERE CAST(COALESCE(json_extract(data, '$.timestamp'), 0) AS INTEGER) < ?`
1410
+ ).run(cutoff)
1411
+ return result.changes
1412
+ }
1413
+
1403
1414
  // --- Connectors ---
1404
1415
  const connectorsStore = createCollectionStore('connectors', { ttlMs: 30_000 })
1405
1416
  export const loadConnectors = connectorsStore.load
@@ -7,6 +7,9 @@ import { WORKSPACE_DIR } from '@/lib/server/data-dir'
7
7
  import { loadConnectors, loadSessions, UPLOAD_DIR } from '@/lib/server/storage'
8
8
  import { errorMessage } from '@/lib/shared-utils'
9
9
  import { isMainSession } from '@/lib/server/agents/main-agent-loop'
10
+ import { log } from '@/lib/server/logger'
11
+
12
+ const TAG = 'task-followups'
10
13
 
11
14
  export { normalizeWhatsappTarget }
12
15
 
@@ -507,7 +510,7 @@ export async function notifyConnectorTaskFollowups(params: {
507
510
  })
508
511
  } catch (err: unknown) {
509
512
  const errMsg = errorMessage(err)
510
- console.warn(`[queue] Failed task follow-up send on connector ${target.connectorId}: ${errMsg}`)
513
+ log.warn(TAG, `Failed task follow-up send on connector ${target.connectorId}: ${errMsg}`)
511
514
  }
512
515
  }
513
516
  }
@@ -71,9 +71,9 @@ const DEFAULT_THRESHOLDS: LoopDetectionThresholds = {
71
71
  pollCritical: 8,
72
72
  pingPongWarn: 3,
73
73
  pingPongCritical: 5,
74
- circuitBreaker: 20,
75
- toolFrequencyWarn: 15,
76
- toolFrequencyCritical: 30,
74
+ circuitBreaker: 15,
75
+ toolFrequencyWarn: 12,
76
+ toolFrequencyCritical: 25,
77
77
  }
78
78
 
79
79
  // ---------------------------------------------------------------------------
@@ -182,6 +182,11 @@ export class ToolLoopTracker {
182
182
  this.keyCount.clear()
183
183
  }
184
184
 
185
+ /** Partial reset: clear per-tool frequency counts but preserve circuit breaker and repeat history. */
186
+ resetFrequencyCounts(): void {
187
+ this.nameCount.clear()
188
+ }
189
+
185
190
  /** Get the full call history (for diagnostics). */
186
191
  getHistory(): ReadonlyArray<ToolCallRecord> {
187
192
  return this.history
@@ -39,6 +39,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
39
39
  capabilities: ['artifact.files'],
40
40
  disciplineGuidance: [
41
41
  'For `files`, include an explicit action whenever possible. Common patterns: `{"action":"list","dirPath":"."}`, `{"action":"read","filePath":"path/to/file.md"}`, and `{"action":"write","files":[{"path":"path/to/file.md","content":"..."}]}`.',
42
+ 'Prefer a single write call with multiple files over writing one file at a time.',
42
43
  ],
43
44
  requestMatchers: [],
44
45
  },
@@ -49,6 +50,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
49
50
  capabilities: ['runtime.shell'],
50
51
  disciplineGuidance: [
51
52
  'For `shell`, use `{"action":"execute","command":"..."}` for commands and `{"action":"status","processId":"..."}` or `{"action":"log","processId":"..."}` for long-lived processes.',
53
+ 'Chain related commands in a single shell call using && to reduce round-trips. Avoid running the same build or test command repeatedly — if it fails, diagnose the error before retrying.',
52
54
  ],
53
55
  requestMatchers: [],
54
56
  },
@@ -59,6 +61,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
59
61
  capabilities: [TOOL_CAPABILITY.researchSearch],
60
62
  disciplineGuidance: [
61
63
  'For `web_search`, use `{"query":"..."}` to research fresh information. For current events, breaking news, or "latest" requests, start with `web_search` before summarizing.',
64
+ 'Gather 2-3 key sources, then synthesize. Do not search-read-search-read in a loop.',
62
65
  ],
63
66
  requestMatchers: [
64
67
  {
@@ -73,6 +76,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
73
76
  capabilities: [TOOL_CAPABILITY.researchFetch],
74
77
  disciplineGuidance: [
75
78
  'For `web_fetch`, use `{"url":"https://..."}` to read a specific page or article after you know the URL.',
79
+ 'Fetch the pages you need, then synthesize. Do not fetch-read-fetch-read in a loop.',
76
80
  ],
77
81
  requestMatchers: [
78
82
  {
@@ -91,6 +95,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
91
95
  'For `browser`, when the task includes a literal URL, pass that exact URL string to `{"action":"navigate","url":"..."}`. Do not invent placeholder URLs like `[Your URL]`, `Example_URL`, or `MockMailPage_URL`.',
92
96
  'For `browser` form work, prefer `{"action":"fill_form","fields":[{"element":"#email","value":"user@example.com"},{"element":"#password","value":"..."}]}`. A shorthand `form` object keyed by input id/name also works for simple forms.',
93
97
  'Use `browser` when the user asks for screenshots, visual proof, page capture, PDFs, or a rendered view of a page. `navigate` alone is not a screenshot.',
98
+ 'Limit browser navigations to what is needed. Each navigation is expensive. Plan your browser session: list the pages you need, visit each once, extract what you need.',
94
99
  ],
95
100
  requestMatchers: [
96
101
  {
@@ -117,6 +122,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
117
122
  'For outbound delivery, inspect available channels with `connector_message_tool` using `{"action":"list_running"}` before claiming something cannot be sent.',
118
123
  'Use `connector_message_tool` with `{"action":"send","message":"...","mediaPath":"..."}` for text/media and `{"action":"send_voice_note","voiceText":"..."}` for voice notes.',
119
124
  'If no channel or recipient is configured, explain that connector/channel setup is missing rather than claiming the capability does not exist.',
125
+ 'Check channel availability once with `list_running`, then send. Do not re-list channels between each message.',
120
126
  ],
121
127
  requestMatchers: [
122
128
  {
@@ -140,6 +146,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
140
146
  capabilities: ['network.http'],
141
147
  disciplineGuidance: [
142
148
  'For `http_request`, send exact literal URLs from the task or from prior tool results. Keep JSON request bodies as raw JSON strings.',
149
+ 'If an API call fails, inspect the error before retrying with the same request. Do not retry the same failing call in a loop.',
143
150
  ],
144
151
  requestMatchers: [],
145
152
  },
@@ -150,6 +157,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
150
157
  capabilities: ['delivery.email'],
151
158
  disciplineGuidance: [
152
159
  'For `email`, send mail with `{"action":"send","to":"user@example.com","subject":"...","body":"..."}`. If delivery depends on SMTP setup, check `{"action":"status"}` before claiming success.',
160
+ 'Compose the full message in one send call. Do not send partial drafts followed by corrections.',
153
161
  ],
154
162
  requestMatchers: [],
155
163
  },
@@ -162,6 +170,7 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
162
170
  'For `google_workspace`, pass exact `gws` arguments in `{"args":[...]}` form. Prefer list/get/read commands first to confirm IDs and current state before mutating Drive, Docs, Sheets, Gmail, Calendar, or Chat resources.',
163
171
  'Use `params` and `jsonInput` for `--params` / `--json` payloads instead of packing raw JSON blobs into the `args` array.',
164
172
  'Do not call interactive `gws auth login` or `gws auth setup` from the agent. Use the extension settings or a pre-authenticated `gws` install.',
173
+ 'Confirm resource IDs with a single list/get call before mutating. Do not repeatedly list the same resources between edits.',
165
174
  ],
166
175
  requestMatchers: [
167
176
  {
@@ -179,6 +188,223 @@ const CORE_TOOL_PLANNING: Record<string, ToolPlanningEntry[]> = {
179
188
  'For `ask_human`, when a workflow needs a code, approval, or out-of-band value from a person, do not guess or keep re-submitting blank forms. Use `{"action":"request_input","question":"..."}` and, for durable pauses, `{"action":"wait_for_reply","correlationId":"..."}`.',
180
189
  'Reuse the same `correlationId` from `request_input` when you call `wait_for_reply`. Once the durable wait returns active, stop the turn immediately and wait for the reply instead of calling `request_input` again.',
181
190
  'Do not ask the same pending human question twice before the durable wait resumes unless the question materially changes.',
191
+ 'Batch related questions into a single request rather than asking one question at a time.',
192
+ ],
193
+ requestMatchers: [],
194
+ },
195
+ ],
196
+
197
+ // --- Internal platform tools ---
198
+
199
+ manage_agents: [
200
+ {
201
+ toolName: 'manage_agents',
202
+ capabilities: ['platform.agents'],
203
+ disciplineGuidance: [
204
+ 'List agents once at the start of a task, then work with specific agent IDs. Do not re-list between each action.',
205
+ ],
206
+ requestMatchers: [],
207
+ },
208
+ ],
209
+ manage_projects: [
210
+ {
211
+ toolName: 'manage_projects',
212
+ capabilities: ['platform.projects'],
213
+ disciplineGuidance: [
214
+ 'List projects once to orient, then operate on specific project IDs. Do not re-list after each update.',
215
+ ],
216
+ requestMatchers: [],
217
+ },
218
+ ],
219
+ manage_tasks: [
220
+ {
221
+ toolName: 'manage_tasks',
222
+ capabilities: ['platform.tasks'],
223
+ disciplineGuidance: [
224
+ 'Read the task list once, make your changes, then move on. Do not re-read the task list after every update.',
225
+ ],
226
+ requestMatchers: [],
227
+ },
228
+ ],
229
+ manage_schedules: [
230
+ {
231
+ toolName: 'manage_schedules',
232
+ capabilities: ['platform.schedules'],
233
+ disciplineGuidance: [
234
+ 'List schedules once to check current state. Do not re-list after each modification.',
235
+ ],
236
+ requestMatchers: [],
237
+ },
238
+ ],
239
+ manage_skills: [
240
+ {
241
+ toolName: 'manage_skills',
242
+ capabilities: ['platform.skills'],
243
+ disciplineGuidance: [
244
+ 'Use `recommend_for_task` to find a relevant skill efficiently. Do not repeatedly list or search skills between each action.',
245
+ ],
246
+ requestMatchers: [],
247
+ },
248
+ ],
249
+ manage_webhooks: [
250
+ {
251
+ toolName: 'manage_webhooks',
252
+ capabilities: ['platform.webhooks'],
253
+ disciplineGuidance: [
254
+ 'List webhooks once for current state. Do not re-list after each change.',
255
+ ],
256
+ requestMatchers: [],
257
+ },
258
+ ],
259
+ manage_secrets: [
260
+ {
261
+ toolName: 'manage_secrets',
262
+ capabilities: ['platform.secrets'],
263
+ disciplineGuidance: [
264
+ 'Store secrets directly. Use the `check` action (not `list`) to verify if a credential already exists before requesting a new one.',
265
+ ],
266
+ requestMatchers: [],
267
+ },
268
+ ],
269
+ manage_chatrooms: [
270
+ {
271
+ toolName: 'manage_chatrooms',
272
+ capabilities: ['platform.chatrooms'],
273
+ disciplineGuidance: [
274
+ 'List chatrooms once to orient, then operate on specific IDs. Do not re-list after each message or update.',
275
+ ],
276
+ requestMatchers: [],
277
+ },
278
+ ],
279
+ manage_protocols: [
280
+ {
281
+ toolName: 'manage_protocols',
282
+ capabilities: ['platform.protocols'],
283
+ disciplineGuidance: [
284
+ 'Read the protocol definition once, then execute steps. Do not re-read the protocol between each step.',
285
+ ],
286
+ requestMatchers: [],
287
+ },
288
+ ],
289
+ manage_platform: [
290
+ {
291
+ toolName: 'manage_platform',
292
+ capabilities: ['platform.umbrella'],
293
+ disciplineGuidance: [
294
+ 'Prefer the direct `manage_*` tools (manage_agents, manage_tasks, etc.) when they are enabled. Use `manage_platform` only as a fallback when the specific tool is not available.',
295
+ ],
296
+ requestMatchers: [],
297
+ },
298
+ ],
299
+ spawn_subagent: [
300
+ {
301
+ toolName: 'spawn_subagent',
302
+ capabilities: ['delegation.subagent'],
303
+ disciplineGuidance: [
304
+ 'Use `waitForCompletion: true` (the default) or `wait`/`wait_all` actions to await results. Do not poll `status` in a loop.',
305
+ 'Batch related delegations — spawn multiple subagents at once if tasks are independent.',
306
+ 'For multi-step or cross-domain work, delegate to a subagent rather than attempting everything in one long tool chain.',
307
+ ],
308
+ requestMatchers: [],
309
+ },
310
+ ],
311
+ delegate: [
312
+ {
313
+ toolName: 'delegate',
314
+ capabilities: ['delegation.cli'],
315
+ disciplineGuidance: [
316
+ 'Give the delegate a complete task description in one call. Do not send incremental instructions across multiple delegation calls.',
317
+ ],
318
+ requestMatchers: [],
319
+ },
320
+ ],
321
+ manage_sessions: [
322
+ {
323
+ toolName: 'sessions_tool',
324
+ capabilities: ['platform.sessions'],
325
+ disciplineGuidance: [
326
+ 'Check session identity once at the start. Do not re-query session info between each action.',
327
+ ],
328
+ requestMatchers: [],
329
+ },
330
+ ],
331
+ memory: [
332
+ {
333
+ toolName: 'memory_tool',
334
+ capabilities: ['memory.search', 'memory.store'],
335
+ disciplineGuidance: [
336
+ 'Search memory once with a good query, then use the results. Do not run multiple overlapping searches for the same topic.',
337
+ 'For stores and updates, write once with complete content. Do not read-back immediately after writing to confirm.',
338
+ ],
339
+ requestMatchers: [],
340
+ },
341
+ ],
342
+ context_mgmt: [
343
+ {
344
+ toolName: 'context_status',
345
+ capabilities: ['context.management'],
346
+ disciplineGuidance: [
347
+ 'Check context status only when you suspect you are running low. Do not check after every tool call.',
348
+ ],
349
+ requestMatchers: [],
350
+ },
351
+ ],
352
+ monitor: [
353
+ {
354
+ toolName: 'monitor_tool',
355
+ capabilities: ['monitoring.watch'],
356
+ disciplineGuidance: [
357
+ 'Prefer `wait_until`, `wait_for_http`, `wait_for_file`, or other `wait_for_*` shortcut actions — they create a durable wait that resumes your turn automatically. Avoid creating a watch with `create_watch` then polling `get_watch` in a loop.',
358
+ ],
359
+ requestMatchers: [],
360
+ },
361
+ ],
362
+ wallet: [
363
+ {
364
+ toolName: 'wallet_tool',
365
+ capabilities: [TOOL_CAPABILITY.walletInspect, TOOL_CAPABILITY.walletExecute],
366
+ disciplineGuidance: [
367
+ 'Inspect wallet state once, then act. Use `simulate_transaction` to validate before executing. Do not re-inspect balances between each operation unless the operation changes them.',
368
+ ],
369
+ requestMatchers: [],
370
+ },
371
+ ],
372
+ image_gen: [
373
+ {
374
+ toolName: 'generate_image',
375
+ capabilities: ['media.image_generation'],
376
+ disciplineGuidance: [
377
+ 'Describe the image fully in one generation call. Do not generate multiple variations unless the user asks for options.',
378
+ ],
379
+ requestMatchers: [],
380
+ },
381
+ ],
382
+ replicate: [
383
+ {
384
+ toolName: 'replicate',
385
+ capabilities: ['media.replicate'],
386
+ disciplineGuidance: [
387
+ 'Submit the job with complete parameters in one call. Use `wait: true` for synchronous completion. If running async, let the built-in polling handle it — do not add your own polling loop on top.',
388
+ ],
389
+ requestMatchers: [],
390
+ },
391
+ ],
392
+ schedule_wake: [
393
+ {
394
+ toolName: 'schedule_wake',
395
+ capabilities: ['runtime.schedule'],
396
+ disciplineGuidance: [
397
+ 'Schedule the wake once with the correct time. Do not reschedule repeatedly to adjust by small increments.',
398
+ ],
399
+ requestMatchers: [],
400
+ },
401
+ ],
402
+ mailbox: [
403
+ {
404
+ toolName: 'mailbox',
405
+ capabilities: ['delivery.mailbox'],
406
+ disciplineGuidance: [
407
+ 'Use `search_messages` for targeted retrieval instead of listing all messages. Do not poll the inbox in a loop waiting for replies.',
182
408
  ],
183
409
  requestMatchers: [],
184
410
  },
@@ -3,6 +3,9 @@
3
3
  */
4
4
 
5
5
  import { sleep, jitteredBackoff } from '@/lib/shared-utils'
6
+ import { log } from '@/lib/server/logger'
7
+
8
+ const TAG = 'tool-retry'
6
9
 
7
10
  export interface RetryOptions {
8
11
  maxAttempts?: number
@@ -51,9 +54,7 @@ export async function withRetry<TArgs>(
51
54
  if (attempt < maxAttempts && isRetryableError(lastResult, retryable)) {
52
55
  await opts?.onRetry?.(attempt, lastResult)
53
56
  const delay = jitteredBackoff(backoffMs, attempt - 1, backoffMs * 16)
54
- console.warn(
55
- `[tool-retry] Attempt ${attempt}/${maxAttempts} matched retryable pattern, retrying in ${delay}ms`,
56
- )
57
+ log.warn(TAG, `Attempt ${attempt}/${maxAttempts} matched retryable pattern, retrying in ${delay}ms`)
57
58
  await sleep(delay)
58
59
  continue
59
60
  }
@@ -84,9 +84,37 @@ export interface GetWalletPortfolioOptions {
84
84
  allowStale?: boolean
85
85
  }
86
86
 
87
+ const PORTFOLIO_CACHE_MAX = 200
87
88
  const portfolioCache = new Map<string, WalletPortfolioCacheEntry>()
88
89
  const evmContractDiscoveryCache = new Map<string, EvmContractDiscoveryCacheEntry>()
89
90
 
91
+ function pruneExpiredPortfolioCaches(): void {
92
+ const now = Date.now()
93
+ for (const [key, entry] of portfolioCache) {
94
+ if (entry.expiresAt <= now) portfolioCache.delete(key)
95
+ }
96
+ for (const [key, entry] of evmContractDiscoveryCache) {
97
+ if (entry.expiresAt <= now) evmContractDiscoveryCache.delete(key)
98
+ }
99
+ // Hard cap as safety net
100
+ if (portfolioCache.size > PORTFOLIO_CACHE_MAX) {
101
+ const excess = portfolioCache.size - PORTFOLIO_CACHE_MAX
102
+ const iter = portfolioCache.keys()
103
+ for (let i = 0; i < excess; i++) {
104
+ const k = iter.next().value
105
+ if (k !== undefined) portfolioCache.delete(k)
106
+ }
107
+ }
108
+ if (evmContractDiscoveryCache.size > PORTFOLIO_CACHE_MAX) {
109
+ const excess = evmContractDiscoveryCache.size - PORTFOLIO_CACHE_MAX
110
+ const iter = evmContractDiscoveryCache.keys()
111
+ for (let i = 0; i < excess; i++) {
112
+ const k = iter.next().value
113
+ if (k !== undefined) evmContractDiscoveryCache.delete(k)
114
+ }
115
+ }
116
+ }
117
+
90
118
  const KNOWN_SOLANA_TOKENS: Record<string, { symbol: string; name: string }> = {
91
119
  EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v: { symbol: 'USDC', name: 'USD Coin' },
92
120
  }
@@ -721,6 +749,7 @@ export async function getWalletPortfolio(wallet: AgentWallet, options?: GetWalle
721
749
  label: `wallet portfolio ${wallet.id}`,
722
750
  })
723
751
 
752
+ pruneExpiredPortfolioCaches()
724
753
  portfolioCache.set(cacheKey, {
725
754
  expiresAt: Date.now() + PORTFOLIO_CACHE_TTL_MS,
726
755
  portfolio,
@@ -2,6 +2,9 @@ import { WebSocketServer, WebSocket } from 'ws'
2
2
  import type { IncomingMessage } from 'http'
3
3
  import { validateAccessKey } from './storage'
4
4
  import { AUTH_COOKIE_NAME, getCookieValue } from '@/lib/auth'
5
+ import { log } from '@/lib/server/logger'
6
+
7
+ const TAG = 'ws-hub'
5
8
 
6
9
  interface WsClient {
7
10
  ws: WebSocket
@@ -67,10 +70,10 @@ export function initWsServer() {
67
70
  })
68
71
 
69
72
  wss.on('error', (err) => {
70
- console.error('[ws-hub] WebSocket server error:', err.message)
73
+ log.error(TAG, 'WebSocket server error:', err.message)
71
74
  })
72
75
 
73
- console.log(`[ws-hub] WebSocket server listening on port ${port}`)
76
+ log.info(TAG, `WebSocket server listening on port ${port}`)
74
77
  }
75
78
 
76
79
  export function closeWsServer(): Promise<void> {