@swarmclawai/swarmclaw 1.2.0 → 1.2.2

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 (241) hide show
  1. package/README.md +19 -0
  2. package/package.json +5 -2
  3. package/skills/coding-agent/SKILL.md +111 -0
  4. package/skills/github/SKILL.md +140 -0
  5. package/skills/nano-banana-pro/SKILL.md +62 -0
  6. package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
  7. package/skills/nano-pdf/SKILL.md +53 -0
  8. package/skills/openai-image-gen/SKILL.md +78 -0
  9. package/skills/openai-image-gen/scripts/gen.py +328 -0
  10. package/skills/resourceful-problem-solving/SKILL.md +49 -0
  11. package/skills/skill-creator/SKILL.md +147 -0
  12. package/skills/skill-creator/scripts/init_skill.py +378 -0
  13. package/skills/skill-creator/scripts/quick_validate.py +159 -0
  14. package/skills/summarize/SKILL.md +77 -0
  15. package/src/app/api/auth/route.ts +20 -5
  16. package/src/app/api/chats/[id]/deploy/route.ts +11 -6
  17. package/src/app/api/chats/[id]/devserver/route.ts +17 -20
  18. package/src/app/api/chats/[id]/messages/route.ts +15 -11
  19. package/src/app/api/chats/[id]/route.ts +9 -10
  20. package/src/app/api/chats/[id]/stop/route.ts +5 -7
  21. package/src/app/api/chats/messages-route.test.ts +8 -6
  22. package/src/app/api/chats/route.ts +9 -10
  23. package/src/app/api/credentials/[id]/route.ts +4 -1
  24. package/src/app/api/extensions/marketplace/route.ts +5 -2
  25. package/src/app/api/ip/route.ts +2 -2
  26. package/src/app/api/memory/maintenance/route.ts +5 -2
  27. package/src/app/api/preview-server/route.ts +15 -12
  28. package/src/app/api/projects/[id]/route.ts +7 -46
  29. package/src/app/api/system/status/route.ts +11 -0
  30. package/src/app/api/upload/route.ts +4 -1
  31. package/src/cli/index.js +7 -0
  32. package/src/cli/spec.js +1 -0
  33. package/src/components/agents/agent-files-editor.tsx +44 -32
  34. package/src/components/agents/personality-builder.tsx +13 -7
  35. package/src/components/agents/trash-list.tsx +1 -1
  36. package/src/components/chat/chat-area.tsx +45 -23
  37. package/src/components/chat/message-bubble.test.ts +35 -0
  38. package/src/components/chat/message-bubble.tsx +20 -9
  39. package/src/components/chat/message-list.tsx +62 -42
  40. package/src/components/chat/swarm-status-card.tsx +10 -3
  41. package/src/components/input/chat-input.tsx +34 -14
  42. package/src/components/layout/daemon-indicator.tsx +7 -8
  43. package/src/components/layout/update-banner.tsx +8 -13
  44. package/src/components/logs/log-list.tsx +1 -1
  45. package/src/components/memory/memory-card.tsx +3 -1
  46. package/src/components/org-chart/org-chart-view.tsx +4 -0
  47. package/src/components/projects/project-list.tsx +4 -2
  48. package/src/components/projects/tabs/overview-tab.tsx +3 -2
  49. package/src/components/secrets/secret-sheet.tsx +1 -1
  50. package/src/components/secrets/secrets-list.tsx +1 -1
  51. package/src/components/shared/agent-switch-dialog.tsx +12 -6
  52. package/src/components/shared/dir-browser.tsx +22 -18
  53. package/src/components/skills/skill-sheet.tsx +2 -3
  54. package/src/components/tasks/task-list.tsx +1 -1
  55. package/src/components/tasks/task-sheet.tsx +1 -1
  56. package/src/hooks/use-openclaw-gateway.ts +46 -27
  57. package/src/instrumentation.ts +10 -7
  58. package/src/lib/chat/assistant-render-id.ts +3 -0
  59. package/src/lib/chat/chat-streaming-state.test.ts +42 -3
  60. package/src/lib/chat/chat-streaming-state.ts +20 -8
  61. package/src/lib/chat/chat.ts +18 -2
  62. package/src/lib/chat/queued-message-queue.test.ts +23 -1
  63. package/src/lib/chat/queued-message-queue.ts +11 -2
  64. package/src/lib/providers/anthropic.ts +6 -3
  65. package/src/lib/providers/claude-cli.ts +9 -3
  66. package/src/lib/providers/cli-utils.test.ts +124 -0
  67. package/src/lib/providers/cli-utils.ts +15 -0
  68. package/src/lib/providers/codex-cli.ts +9 -3
  69. package/src/lib/providers/gemini-cli.ts +6 -2
  70. package/src/lib/providers/index.ts +4 -1
  71. package/src/lib/providers/ollama.ts +5 -2
  72. package/src/lib/providers/openai.ts +8 -5
  73. package/src/lib/providers/opencode-cli.ts +6 -2
  74. package/src/lib/server/activity/activity-log.ts +21 -0
  75. package/src/lib/server/agents/agent-availability.test.ts +10 -5
  76. package/src/lib/server/agents/agent-cascade.ts +79 -59
  77. package/src/lib/server/agents/agent-registry.ts +23 -4
  78. package/src/lib/server/agents/agent-repository.ts +90 -0
  79. package/src/lib/server/agents/delegation-job-repository.ts +53 -0
  80. package/src/lib/server/agents/delegation-jobs.ts +11 -4
  81. package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
  82. package/src/lib/server/agents/guardian.ts +2 -2
  83. package/src/lib/server/agents/main-agent-loop.ts +14 -6
  84. package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
  85. package/src/lib/server/agents/subagent-runtime.ts +9 -6
  86. package/src/lib/server/agents/subagent-swarm.ts +3 -2
  87. package/src/lib/server/agents/task-session.ts +3 -4
  88. package/src/lib/server/approvals/approval-repository.ts +30 -0
  89. package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
  90. package/src/lib/server/autonomy/supervisor-reflection.ts +14 -1
  91. package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
  92. package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
  93. package/src/lib/server/chat-execution/chat-execution.ts +84 -1914
  94. package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
  95. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
  96. package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
  97. package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
  98. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
  99. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
  100. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -3
  101. package/src/lib/server/chat-execution/continuation-limits.ts +6 -3
  102. package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
  103. package/src/lib/server/chat-execution/message-classifier.ts +5 -2
  104. package/src/lib/server/chat-execution/post-stream-finalization.ts +5 -2
  105. package/src/lib/server/chat-execution/prompt-builder.ts +22 -1
  106. package/src/lib/server/chat-execution/prompt-sections.ts +55 -13
  107. package/src/lib/server/chat-execution/response-completeness.ts +5 -2
  108. package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
  109. package/src/lib/server/chat-execution/stream-agent-chat.ts +58 -25
  110. package/src/lib/server/chatrooms/chatroom-memory-bridge.ts +6 -3
  111. package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
  112. package/src/lib/server/connectors/bluebubbles.ts +7 -4
  113. package/src/lib/server/connectors/connector-inbound.ts +16 -13
  114. package/src/lib/server/connectors/connector-lifecycle.ts +11 -8
  115. package/src/lib/server/connectors/connector-outbound.ts +6 -3
  116. package/src/lib/server/connectors/connector-repository.ts +58 -0
  117. package/src/lib/server/connectors/discord.ts +10 -7
  118. package/src/lib/server/connectors/email.ts +17 -14
  119. package/src/lib/server/connectors/googlechat.ts +7 -4
  120. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -2
  121. package/src/lib/server/connectors/matrix.ts +6 -3
  122. package/src/lib/server/connectors/openclaw.ts +20 -17
  123. package/src/lib/server/connectors/outbox.ts +4 -1
  124. package/src/lib/server/connectors/runtime-state.test.ts +117 -0
  125. package/src/lib/server/connectors/runtime-state.ts +19 -0
  126. package/src/lib/server/connectors/session-consolidation.ts +5 -2
  127. package/src/lib/server/connectors/signal.ts +9 -6
  128. package/src/lib/server/connectors/slack.ts +13 -10
  129. package/src/lib/server/connectors/teams.ts +8 -5
  130. package/src/lib/server/connectors/telegram.ts +15 -12
  131. package/src/lib/server/connectors/whatsapp.ts +32 -29
  132. package/src/lib/server/credentials/credential-repository.ts +7 -0
  133. package/src/lib/server/embeddings.ts +4 -1
  134. package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
  135. package/src/lib/server/link-understanding.ts +4 -1
  136. package/src/lib/server/memory/memory-abstract.test.ts +59 -0
  137. package/src/lib/server/memory/memory-abstract.ts +59 -0
  138. package/src/lib/server/memory/memory-db.ts +40 -14
  139. package/src/lib/server/missions/mission-repository.ts +74 -0
  140. package/src/lib/server/missions/mission-service/actions.ts +6 -0
  141. package/src/lib/server/missions/mission-service/bindings.ts +9 -0
  142. package/src/lib/server/missions/mission-service/context.ts +4 -0
  143. package/src/lib/server/missions/mission-service/core.ts +2269 -0
  144. package/src/lib/server/missions/mission-service/queries.ts +12 -0
  145. package/src/lib/server/missions/mission-service/recovery.ts +5 -0
  146. package/src/lib/server/missions/mission-service/ticks.ts +9 -0
  147. package/src/lib/server/missions/mission-service.test.ts +9 -2
  148. package/src/lib/server/missions/mission-service.ts +6 -2263
  149. package/src/lib/server/openclaw/gateway.ts +8 -5
  150. package/src/lib/server/persistence/repository-utils.ts +154 -0
  151. package/src/lib/server/persistence/storage-context.ts +51 -0
  152. package/src/lib/server/persistence/transaction.ts +1 -0
  153. package/src/lib/server/project-utils.ts +13 -0
  154. package/src/lib/server/projects/project-repository.ts +36 -0
  155. package/src/lib/server/projects/project-service.ts +79 -0
  156. package/src/lib/server/protocols/protocol-agent-turn.ts +5 -2
  157. package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
  158. package/src/lib/server/protocols/protocol-run-lifecycle.ts +5 -2
  159. package/src/lib/server/protocols/protocol-step-helpers.ts +4 -1
  160. package/src/lib/server/provider-health.ts +18 -0
  161. package/src/lib/server/query-expansion.ts +4 -1
  162. package/src/lib/server/runtime/alert-dispatch.ts +8 -7
  163. package/src/lib/server/runtime/daemon-policy.ts +1 -1
  164. package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
  165. package/src/lib/server/runtime/daemon-state/health.ts +6 -0
  166. package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
  167. package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
  168. package/src/lib/server/runtime/daemon-state.test.ts +48 -0
  169. package/src/lib/server/runtime/daemon-state.ts +3 -1331
  170. package/src/lib/server/runtime/estop-repository.ts +4 -0
  171. package/src/lib/server/runtime/estop.ts +3 -1
  172. package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
  173. package/src/lib/server/runtime/heartbeat-service.ts +78 -34
  174. package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
  175. package/src/lib/server/runtime/idle-window.ts +6 -3
  176. package/src/lib/server/runtime/network.ts +11 -0
  177. package/src/lib/server/runtime/orchestrator-events.ts +2 -2
  178. package/src/lib/server/runtime/perf.ts +4 -1
  179. package/src/lib/server/runtime/process-manager.ts +7 -4
  180. package/src/lib/server/runtime/queue/claims.ts +4 -0
  181. package/src/lib/server/runtime/queue/core.ts +2079 -0
  182. package/src/lib/server/runtime/queue/execution.ts +7 -0
  183. package/src/lib/server/runtime/queue/followups.ts +4 -0
  184. package/src/lib/server/runtime/queue/queries.ts +12 -0
  185. package/src/lib/server/runtime/queue/recovery.ts +7 -0
  186. package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
  187. package/src/lib/server/runtime/queue-repository.ts +17 -0
  188. package/src/lib/server/runtime/queue.ts +5 -2058
  189. package/src/lib/server/runtime/run-ledger.ts +6 -5
  190. package/src/lib/server/runtime/run-repository.ts +73 -0
  191. package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
  192. package/src/lib/server/runtime/runtime-settings.ts +1 -1
  193. package/src/lib/server/runtime/runtime-state.ts +99 -0
  194. package/src/lib/server/runtime/scheduler.ts +13 -8
  195. package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
  196. package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
  197. package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
  198. package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
  199. package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
  200. package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
  201. package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
  202. package/src/lib/server/runtime/session-run-manager.ts +72 -1374
  203. package/src/lib/server/runtime/watch-job-repository.ts +35 -0
  204. package/src/lib/server/runtime/watch-jobs.ts +3 -1
  205. package/src/lib/server/sandbox/bridge-auth-registry.ts +6 -0
  206. package/src/lib/server/sandbox/novnc-auth.ts +10 -0
  207. package/src/lib/server/schedules/schedule-repository.ts +42 -0
  208. package/src/lib/server/session-tools/context.ts +14 -0
  209. package/src/lib/server/session-tools/discovery.ts +9 -6
  210. package/src/lib/server/session-tools/index.ts +3 -1
  211. package/src/lib/server/session-tools/platform.ts +1 -1
  212. package/src/lib/server/session-tools/subagent.ts +23 -2
  213. package/src/lib/server/session-tools/wallet.ts +4 -1
  214. package/src/lib/server/sessions/session-repository.ts +85 -0
  215. package/src/lib/server/settings/settings-repository.ts +25 -0
  216. package/src/lib/server/skills/clawhub-client.ts +4 -1
  217. package/src/lib/server/skills/runtime-skill-resolver.ts +8 -2
  218. package/src/lib/server/skills/skill-discovery.test.ts +2 -2
  219. package/src/lib/server/skills/skill-discovery.ts +2 -2
  220. package/src/lib/server/skills/skill-eligibility.ts +6 -0
  221. package/src/lib/server/skills/skill-repository.ts +14 -0
  222. package/src/lib/server/solana.ts +6 -0
  223. package/src/lib/server/storage-auth.ts +5 -5
  224. package/src/lib/server/storage-normalization.ts +4 -0
  225. package/src/lib/server/storage.ts +32 -32
  226. package/src/lib/server/tasks/task-followups.ts +4 -1
  227. package/src/lib/server/tasks/task-repository.ts +54 -0
  228. package/src/lib/server/tool-loop-detection.ts +8 -3
  229. package/src/lib/server/tool-planning.ts +226 -0
  230. package/src/lib/server/tool-retry.ts +4 -3
  231. package/src/lib/server/usage/usage-repository.ts +30 -0
  232. package/src/lib/server/wallet/wallet-portfolio.ts +29 -0
  233. package/src/lib/server/webhooks/webhook-repository.ts +10 -0
  234. package/src/lib/server/ws-hub.ts +5 -2
  235. package/src/lib/strip-internal-metadata.test.ts +78 -37
  236. package/src/lib/strip-internal-metadata.ts +20 -6
  237. package/src/stores/use-approval-store.ts +7 -1
  238. package/src/stores/use-chat-store.test.ts +54 -0
  239. package/src/stores/use-chat-store.ts +26 -6
  240. package/src/types/index.ts +6 -0
  241. /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
@@ -1,12 +1,13 @@
1
1
  import fs from 'fs'
2
2
  import path from 'path'
3
3
  import crypto from 'crypto'
4
- import os from 'os'
5
- import type { ChildProcess } from 'node:child_process'
6
4
  import Database from 'better-sqlite3'
7
5
 
8
6
  import { perf } from '@/lib/server/runtime/perf'
7
+ import { log } from '@/lib/server/logger'
9
8
  import { notify } from '@/lib/server/ws-hub'
9
+
10
+ const TAG = 'storage'
10
11
  import { DATA_DIR, IS_BUILD_BOOTSTRAP, WORKSPACE_DIR } from './data-dir'
11
12
  import { normalizeHeartbeatSettingFields } from '@/lib/runtime/heartbeat-defaults'
12
13
  import { normalizeRuntimeSettingFields } from '@/lib/runtime/runtime-loop'
@@ -102,11 +103,6 @@ export function withTransaction<T>(fn: () => T): T {
102
103
  type StoredObject = Record<string, unknown>
103
104
  type StoredSessionRecord = Session
104
105
  type StoredAgentRecord = Agent
105
- type ActiveProcess = ChildProcess | {
106
- runId?: string | null
107
- source?: string
108
- kill: (signal?: NodeJS.Signals | number) => boolean | void
109
- }
110
106
 
111
107
  // Collection tables (id → JSON blob)
112
108
  const COLLECTIONS = [
@@ -264,8 +260,8 @@ function saveCollection(table: string, data: Record<string, unknown>) {
264
260
  // partial collection instead of a full load-modify-save. This prevents
265
261
  // accidental data wipes (e.g. tests calling saveCredentials with 1 item).
266
262
  if (toDelete.length > 0 && next.size > 0 && toDelete.length > next.size) {
267
- console.error(
268
- `[storage] BLOCKED destructive saveCollection("${table}"): ` +
263
+ log.error(TAG,
264
+ `BLOCKED destructive saveCollection("${table}"): ` +
269
265
  `would delete ${toDelete.length} rows but only upsert ${next.size}. ` +
270
266
  `Use deleteCollectionItem() for explicit deletes or load-modify-save to update.`,
271
267
  )
@@ -303,7 +299,18 @@ function deleteCollectionItem(table: string, id: string) {
303
299
  db.prepare(`DELETE FROM ${table} WHERE id = ?`).run(id)
304
300
  const cached = collectionCache.get(table)
305
301
  if (cached) cached.delete(id)
302
+ invalidateDerivedCollectionCaches(table)
303
+ }
304
+
305
+ function invalidateDerivedCollectionCaches(table: string): void {
306
306
  factoryTtlCaches.get(table)?.invalidate()
307
+ if (table === 'sessions') {
308
+ getSessionsCache().invalidate()
309
+ return
310
+ }
311
+ if (table === 'agents') {
312
+ getAgentsCache().invalidate()
313
+ }
307
314
  }
308
315
 
309
316
  /**
@@ -319,7 +326,7 @@ function upsertCollectionItem(table: string, id: string, value: unknown) {
319
326
  if (cached) {
320
327
  cached.set(id, serialized)
321
328
  }
322
- factoryTtlCaches.get(table)?.invalidate()
329
+ invalidateDerivedCollectionCaches(table)
323
330
  }
324
331
 
325
332
  function loadCollectionItem(table: string, id: string): unknown | null {
@@ -353,7 +360,7 @@ function upsertCollectionItems(table: string, entries: Array<[string, unknown]>)
353
360
  cached.set(id, serialized)
354
361
  }
355
362
  }
356
- factoryTtlCaches.get(table)?.invalidate()
363
+ invalidateDerivedCollectionCaches(table)
357
364
  }
358
365
 
359
366
  export function loadStoredItem(table: StorageCollection, id: string): unknown | null {
@@ -537,7 +544,7 @@ const MIGRATION_FLAG = path.join(DATA_DIR, '.sqlite_migrated')
537
544
  function migrateFromJson() {
538
545
  if (fs.existsSync(MIGRATION_FLAG)) return
539
546
 
540
- console.log('[storage] Migrating from JSON files to SQLite...')
547
+ log.info(TAG, 'Migrating from JSON files to SQLite...')
541
548
 
542
549
  const transaction = db.transaction(() => {
543
550
  for (const [table, jsonPath] of Object.entries(JSON_FILES)) {
@@ -549,7 +556,7 @@ function migrateFromJson() {
549
556
  for (const [id, val] of Object.entries(data)) {
550
557
  ins.run(id, JSON.stringify(val))
551
558
  }
552
- console.log(`[storage] Migrated ${table}: ${Object.keys(data).length} records`)
559
+ log.info(TAG, `Migrated ${table}: ${Object.keys(data).length} records`)
553
560
  }
554
561
  } catch { /* skip malformed files */ }
555
562
  }
@@ -562,7 +569,7 @@ function migrateFromJson() {
562
569
  const data = JSON.parse(fs.readFileSync(settingsPath, 'utf8'))
563
570
  if (data && Object.keys(data).length > 0) {
564
571
  saveSingleton('settings', data)
565
- console.log('[storage] Migrated settings')
572
+ log.info(TAG, 'Migrated settings')
566
573
  }
567
574
  } catch { /* skip */ }
568
575
  }
@@ -574,7 +581,7 @@ function migrateFromJson() {
574
581
  const data = JSON.parse(fs.readFileSync(queuePath, 'utf8'))
575
582
  if (Array.isArray(data) && data.length > 0) {
576
583
  saveSingleton('queue', data)
577
- console.log(`[storage] Migrated queue: ${data.length} items`)
584
+ log.info(TAG, `Migrated queue: ${data.length} items`)
578
585
  }
579
586
  } catch { /* skip */ }
580
587
  }
@@ -592,14 +599,14 @@ function migrateFromJson() {
592
599
  }
593
600
  }
594
601
  }
595
- console.log('[storage] Migrated usage records')
602
+ log.info(TAG, 'Migrated usage records')
596
603
  } catch { /* skip */ }
597
604
  }
598
605
  })
599
606
 
600
607
  transaction()
601
608
  fs.writeFileSync(MIGRATION_FLAG, new Date().toISOString())
602
- console.log('[storage] Migration complete. JSON files preserved as backup.')
609
+ log.info(TAG, 'Migration complete. JSON files preserved as backup.')
603
610
  }
604
611
 
605
612
  if (!IS_BUILD_BOOTSTRAP) {
@@ -1400,6 +1407,14 @@ export function appendUsage(sessionId: string, record: unknown) {
1400
1407
  ins.run(sessionId, JSON.stringify(record))
1401
1408
  }
1402
1409
 
1410
+ export function pruneOldUsage(maxAgeMs: number): number {
1411
+ const cutoff = Date.now() - maxAgeMs
1412
+ const result = db.prepare(
1413
+ `DELETE FROM usage WHERE CAST(COALESCE(json_extract(data, '$.timestamp'), 0) AS INTEGER) < ?`
1414
+ ).run(cutoff)
1415
+ return result.changes
1416
+ }
1417
+
1403
1418
  // --- Connectors ---
1404
1419
  const connectorsStore = createCollectionStore('connectors', { ttlMs: 30_000 })
1405
1420
  export const loadConnectors = connectorsStore.load
@@ -1427,21 +1442,6 @@ const webhooksStore = createCollectionStore('webhooks')
1427
1442
  export const loadWebhooks = webhooksStore.load
1428
1443
  export const saveWebhooks = webhooksStore.save
1429
1444
 
1430
- // --- Active processes ---
1431
- export const active = new Map<string, ActiveProcess>()
1432
- export const devServers = new Map<string, { proc: ChildProcess; url: string }>()
1433
-
1434
- // --- Utilities ---
1435
- export function localIP(): string {
1436
- for (const ifaces of Object.values(os.networkInterfaces())) {
1437
- if (!ifaces) continue
1438
- for (const i of ifaces) {
1439
- if (i.family === 'IPv4' && !i.internal) return i.address
1440
- }
1441
- }
1442
- return 'localhost'
1443
- }
1444
-
1445
1445
  // --- MCP Servers ---
1446
1446
  const mcpServersStore = createCollectionStore('mcp_servers')
1447
1447
  export const loadMcpServers = mcpServersStore.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
  }
@@ -0,0 +1,54 @@
1
+ import type { BoardTask } from '@/types'
2
+
3
+ import {
4
+ deleteTask as deleteStoredTask,
5
+ loadTask as loadStoredTask,
6
+ loadTasks as loadStoredTasks,
7
+ patchTask as patchStoredTask,
8
+ saveTasks as saveStoredTasks,
9
+ upsertTask as upsertStoredTask,
10
+ upsertTasks as upsertStoredTasks,
11
+ } from '@/lib/server/storage'
12
+ import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
13
+
14
+ export const taskRepository = createRecordRepository<BoardTask>(
15
+ 'tasks',
16
+ {
17
+ get(id) {
18
+ return loadStoredTask(id) as BoardTask | null
19
+ },
20
+ list() {
21
+ return loadStoredTasks() as Record<string, BoardTask>
22
+ },
23
+ upsert(id, value) {
24
+ upsertStoredTask(id, value as BoardTask)
25
+ },
26
+ upsertMany(entries) {
27
+ upsertStoredTasks(entries as Array<[string, BoardTask]>)
28
+ },
29
+ replace(data) {
30
+ saveStoredTasks(data as Record<string, BoardTask>)
31
+ },
32
+ patch(id, updater) {
33
+ return patchStoredTask(id, updater as (current: BoardTask | null) => BoardTask | null) as BoardTask | null
34
+ },
35
+ delete(id) {
36
+ deleteStoredTask(id)
37
+ },
38
+ },
39
+ )
40
+
41
+ export const getTask = (id: string) => taskRepository.get(id)
42
+ export const getTasks = (ids: string[]) => taskRepository.getMany(ids)
43
+ export const listTasks = () => taskRepository.list()
44
+ export const saveTask = (id: string, task: BoardTask | Record<string, unknown>) => taskRepository.upsert(id, task as BoardTask)
45
+ export const saveTaskMany = (entries: Array<[string, BoardTask | Record<string, unknown>]>) => taskRepository.upsertMany(entries as Array<[string, BoardTask]>)
46
+ export const replaceTasks = (items: Record<string, BoardTask | Record<string, unknown>>) => taskRepository.replace(items as Record<string, BoardTask>)
47
+ export const patchTask = (id: string, updater: (current: BoardTask | null) => BoardTask | null) => taskRepository.patch(id, updater)
48
+ export const deleteTask = (id: string) => taskRepository.delete(id)
49
+
50
+ export const loadTasks = listTasks
51
+ export const loadTask = getTask
52
+ export const saveTasks = replaceTasks
53
+ export const upsertTask = saveTask
54
+ export const upsertTasks = saveTaskMany
@@ -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
  }
@@ -0,0 +1,30 @@
1
+ import type { UsageRecord } from '@/types'
2
+
3
+ import {
4
+ appendUsage as appendStoredUsage,
5
+ getUsageSpendSince as getStoredUsageSpendSince,
6
+ loadUsage as loadStoredUsage,
7
+ pruneOldUsage as pruneStoredUsage,
8
+ saveUsage as saveStoredUsage,
9
+ } from '@/lib/server/storage'
10
+ import { perf } from '@/lib/server/runtime/perf'
11
+
12
+ export function loadUsage(): Record<string, UsageRecord[]> {
13
+ return perf.measureSync('repository', 'usage.list', () => loadStoredUsage())
14
+ }
15
+
16
+ export function saveUsage(data: Record<string, UsageRecord[]>): void {
17
+ perf.measureSync('repository', 'usage.replace', () => saveStoredUsage(data), { count: Object.keys(data).length })
18
+ }
19
+
20
+ export function appendUsage(sessionId: string, record: unknown): void {
21
+ perf.measureSync('repository', 'usage.append', () => appendStoredUsage(sessionId, record), { sessionId })
22
+ }
23
+
24
+ export function getUsageSpendSince(minTimestamp: number): number {
25
+ return perf.measureSync('repository', 'usage.spendSince', () => getStoredUsageSpendSince(minTimestamp), { minTimestamp })
26
+ }
27
+
28
+ export function pruneOldUsage(maxAgeMs: number): number {
29
+ return perf.measureSync('repository', 'usage.prune', () => pruneStoredUsage(maxAgeMs), { maxAgeMs })
30
+ }
@@ -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,
@@ -0,0 +1,10 @@
1
+ export {
2
+ appendWebhookLog,
3
+ deleteWebhookRetry,
4
+ loadWebhookLogs,
5
+ loadWebhookRetryQueue,
6
+ loadWebhooks,
7
+ saveWebhookRetryQueue,
8
+ saveWebhooks,
9
+ upsertWebhookRetry,
10
+ } from '@/lib/server/storage'