@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
@@ -0,0 +1,35 @@
1
+ import type { WatchJob } from '@/types'
2
+
3
+ import {
4
+ deleteWatchJob as deleteStoredWatchJob,
5
+ loadWatchJobs as loadStoredWatchJobs,
6
+ upsertWatchJob as upsertStoredWatchJob,
7
+ upsertWatchJobs as upsertStoredWatchJobs,
8
+ } from '@/lib/server/storage'
9
+ import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
10
+
11
+ export const watchJobRepository = createRecordRepository<WatchJob>(
12
+ 'watch-jobs',
13
+ {
14
+ get(id) {
15
+ return (loadStoredWatchJobs() as Record<string, WatchJob>)[id] || null
16
+ },
17
+ list() {
18
+ return loadStoredWatchJobs() as Record<string, WatchJob>
19
+ },
20
+ upsert(id, value) {
21
+ upsertStoredWatchJob(id, value as WatchJob)
22
+ },
23
+ upsertMany(entries) {
24
+ upsertStoredWatchJobs(entries as Array<[string, WatchJob]>)
25
+ },
26
+ delete(id) {
27
+ deleteStoredWatchJob(id)
28
+ },
29
+ },
30
+ )
31
+
32
+ export const loadWatchJobs = () => watchJobRepository.list()
33
+ export const upsertWatchJob = (id: string, value: WatchJob | Record<string, unknown>) => watchJobRepository.upsert(id, value as WatchJob)
34
+ export const upsertWatchJobs = (entries: Array<[string, WatchJob | Record<string, unknown>]>) => watchJobRepository.upsertMany(entries as Array<[string, WatchJob]>)
35
+ export const deleteWatchJob = (id: string) => watchJobRepository.delete(id)
@@ -5,7 +5,9 @@ import { genId } from '@/lib/id'
5
5
  import type { MailboxEnvelope, WatchJob } from '@/types'
6
6
  import { dispatchWake } from '@/lib/server/runtime/wake-dispatcher'
7
7
  import { enqueueSystemEvent } from '@/lib/server/runtime/system-events'
8
- import { loadApprovals, loadTasks, loadWatchJobs, upsertWatchJob, upsertWatchJobs } from '@/lib/server/storage'
8
+ import { loadApprovals } from '@/lib/server/approvals/approval-repository'
9
+ import { loadTasks } from '@/lib/server/tasks/task-repository'
10
+ import { loadWatchJobs, upsertWatchJob, upsertWatchJobs } from '@/lib/server/runtime/watch-job-repository'
9
11
  import { notify } from '@/lib/server/ws-hub'
10
12
  import { fetchMailboxMessages, getMailboxHighwaterUid } from '@/lib/server/chatrooms/mailbox-utils'
11
13
  import { errorMessage } from '@/lib/shared-utils'
@@ -3,10 +3,16 @@ type BridgeAuth = {
3
3
  password?: string
4
4
  }
5
5
 
6
+ const AUTH_BY_PORT_MAX = 200
6
7
  const authByPort = new Map<number, BridgeAuth>()
7
8
 
8
9
  export function setBridgeAuthForPort(port: number, auth: BridgeAuth): void {
9
10
  if (!Number.isFinite(port) || port <= 0) return
11
+ // FIFO eviction at cap
12
+ if (!authByPort.has(port) && authByPort.size >= AUTH_BY_PORT_MAX) {
13
+ const firstKey = authByPort.keys().next().value
14
+ if (firstKey !== undefined) authByPort.delete(firstKey)
15
+ }
10
16
  const token = typeof auth.token === 'string' ? auth.token.trim() : ''
11
17
  const password = typeof auth.password === 'string' ? auth.password.trim() : ''
12
18
  authByPort.set(port, {
@@ -15,12 +15,22 @@ export type NoVncObserverTokenPayload = {
15
15
  password?: string
16
16
  }
17
17
 
18
+ const NOVNC_TOKEN_MAX = 500
18
19
  const noVncObserverTokens = new Map<string, NoVncObserverTokenEntry>()
19
20
 
20
21
  function pruneExpiredObserverTokens(now: number): void {
21
22
  for (const [token, entry] of noVncObserverTokens) {
22
23
  if (entry.expiresAt <= now) noVncObserverTokens.delete(token)
23
24
  }
25
+ // Hard cap as safety net
26
+ if (noVncObserverTokens.size > NOVNC_TOKEN_MAX) {
27
+ const excess = noVncObserverTokens.size - NOVNC_TOKEN_MAX
28
+ const iter = noVncObserverTokens.keys()
29
+ for (let i = 0; i < excess; i++) {
30
+ const k = iter.next().value
31
+ if (k !== undefined) noVncObserverTokens.delete(k)
32
+ }
33
+ }
24
34
  }
25
35
 
26
36
  export function isNoVncEnabled(params: { enableNoVnc: boolean; headless: boolean }): boolean {
@@ -0,0 +1,42 @@
1
+ import type { Schedule } from '@/types'
2
+
3
+ import {
4
+ deleteSchedule as deleteStoredSchedule,
5
+ loadSchedule as loadStoredSchedule,
6
+ loadSchedules as loadStoredSchedules,
7
+ saveSchedules as saveStoredSchedules,
8
+ upsertSchedule as upsertStoredSchedule,
9
+ upsertSchedules as upsertStoredSchedules,
10
+ } from '@/lib/server/storage'
11
+ import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
12
+
13
+ export const scheduleRepository = createRecordRepository<Schedule>(
14
+ 'schedules',
15
+ {
16
+ get(id) {
17
+ return loadStoredSchedule(id) as Schedule | null
18
+ },
19
+ list() {
20
+ return loadStoredSchedules() as Record<string, Schedule>
21
+ },
22
+ upsert(id, value) {
23
+ upsertStoredSchedule(id, value as Schedule)
24
+ },
25
+ upsertMany(entries) {
26
+ upsertStoredSchedules(entries as Array<[string, Schedule]>)
27
+ },
28
+ replace(data) {
29
+ saveStoredSchedules(data as Record<string, Schedule>)
30
+ },
31
+ delete(id) {
32
+ deleteStoredSchedule(id)
33
+ },
34
+ },
35
+ )
36
+
37
+ export const loadSchedules = () => scheduleRepository.list()
38
+ export const loadSchedule = (id: string) => scheduleRepository.get(id)
39
+ export const saveSchedules = (items: Record<string, Schedule | Record<string, unknown>>) => scheduleRepository.replace(items as Record<string, Schedule>)
40
+ export const upsertSchedule = (id: string, value: Schedule | Record<string, unknown>) => scheduleRepository.upsert(id, value as Schedule)
41
+ export const upsertSchedules = (entries: Array<[string, Schedule | Record<string, unknown>]>) => scheduleRepository.upsertMany(entries as Array<[string, Schedule]>)
42
+ export const deleteSchedule = (id: string) => scheduleRepository.delete(id)
@@ -164,6 +164,7 @@ export function extractResumeIdentifier(text: string): string | null {
164
164
  return null
165
165
  }
166
166
 
167
+ const BINARY_LOOKUP_CACHE_MAX = 100
167
168
  const binaryLookupCache = new Map<string, { checkedAt: number; path: string | null }>()
168
169
  const BINARY_LOOKUP_TTL_MS = 30_000
169
170
  const isWindows = process.platform === 'win32'
@@ -173,6 +174,19 @@ export function findBinaryOnPath(binaryName: string): string | null {
173
174
  const cached = binaryLookupCache.get(binaryName)
174
175
  if (cached && now - cached.checkedAt < BINARY_LOOKUP_TTL_MS) return cached.path
175
176
 
177
+ // Prune expired + cap
178
+ for (const [k, v] of binaryLookupCache) {
179
+ if (now - v.checkedAt > BINARY_LOOKUP_TTL_MS) binaryLookupCache.delete(k)
180
+ }
181
+ if (binaryLookupCache.size > BINARY_LOOKUP_CACHE_MAX) {
182
+ const excess = binaryLookupCache.size - BINARY_LOOKUP_CACHE_MAX
183
+ const iter = binaryLookupCache.keys()
184
+ for (let i = 0; i < excess; i++) {
185
+ const k = iter.next().value
186
+ if (k !== undefined) binaryLookupCache.delete(k)
187
+ }
188
+ }
189
+
176
190
  const { spawnSync } = require('child_process')
177
191
  const probe = isWindows
178
192
  ? spawnSync('where', [binaryName], { encoding: 'utf-8', timeout: 2000, stdio: 'pipe' })
@@ -11,6 +11,9 @@ import { loadSessions, patchAgent, patchSession } from '../storage'
11
11
  import { inferExtensionPublisherSourceFromUrl } from '@/lib/extension-sources'
12
12
  import { errorMessage } from '@/lib/shared-utils'
13
13
  import { getEnabledCapabilityIds, isExternalExtensionId, normalizeCapabilitySelection } from '@/lib/capability-selection'
14
+ import { log } from '@/lib/server/logger'
15
+
16
+ const TAG = 'session-tools-discovery'
14
17
 
15
18
  function grantCapabilitySelection(current: {
16
19
  tools?: string[] | null
@@ -62,7 +65,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
62
65
  const q = typeof normalized.query === 'string' ? normalized.query : ''
63
66
  const extensionId = explicitExtensionId || (action === 'request_access' ? q.trim() : '')
64
67
 
65
- console.log('[discovery] Executing action:', action, { query: q, extensionId })
68
+ log.info(TAG, 'Executing action:', { action, query: q, extensionId })
66
69
 
67
70
  try {
68
71
  switch (action) {
@@ -87,7 +90,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
87
90
  const results: Record<string, unknown>[] = []
88
91
 
89
92
  try {
90
- console.log('[discovery] Searching ClawHub...')
93
+ log.info(TAG, 'Searching ClawHub...')
91
94
  const hubResults = await searchClawHub(q)
92
95
  if (hubResults && hubResults.skills) {
93
96
  results.push(...hubResults.skills.map((s) => ({
@@ -101,11 +104,11 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
101
104
  })))
102
105
  }
103
106
  } catch (err: unknown) {
104
- console.error('[discovery] ClawHub search failed:', errorMessage(err))
107
+ log.error(TAG, 'ClawHub search failed:', errorMessage(err))
105
108
  }
106
109
 
107
110
  try {
108
- console.log('[discovery] Searching SwarmClaw registry...')
111
+ log.info(TAG, 'Searching SwarmClaw registry...')
109
112
  const registryResults = new Map<string, Record<string, unknown>>()
110
113
  const registries = [
111
114
  { url: 'https://swarmclaw.ai/registry/extensions.json', catalogSource: 'swarmclaw-site' },
@@ -133,7 +136,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
133
136
  }
134
137
  results.push(...registryResults.values())
135
138
  } catch (err: unknown) {
136
- console.error('[discovery] SC Registry search failed:', errorMessage(err))
139
+ log.error(TAG, 'SC Registry search failed:', errorMessage(err))
137
140
  }
138
141
 
139
142
  if (results.length === 0) {
@@ -226,7 +229,7 @@ async function executeDiscoveryAction(args: Record<string, unknown>, bctx?: Tool
226
229
  }
227
230
  } catch (err: unknown) {
228
231
  const msg = errorMessage(err)
229
- console.error('[discovery] executeDiscoveryAction failed:', msg)
232
+ log.error(TAG, 'executeDiscoveryAction failed:', msg)
230
233
  return `Error: ${msg}`
231
234
  }
232
235
  }
@@ -57,6 +57,8 @@ import {
57
57
  export type { ToolContext, SessionToolsResult }
58
58
  export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
59
59
 
60
+ const TAG = 'session-tools'
61
+
60
62
  const DELEGATION_TOOL_NAMES = new Set([
61
63
  'delegate',
62
64
  'spawn_subagent',
@@ -455,7 +457,7 @@ export async function buildSessionTools(cwd: string, enabledExtensions: string[]
455
457
  abortSignalRef,
456
458
  }
457
459
  } catch (err: any) {
458
- console.error('[session-tools] buildSessionTools critical failure:', err.message)
460
+ log.error(TAG, 'buildSessionTools critical failure:', err.message)
459
461
  throw err
460
462
  }
461
463
  }
@@ -226,7 +226,7 @@ const PlatformExtension: Extension = {
226
226
  description: 'Unified management of agents, projects, tasks, schedules, skills, documents, and secrets.',
227
227
  hooks: {
228
228
  getCapabilityDescription: () => 'I can manage durable execution context across agents, projects, tasks, schedules, documents, skills, webhooks, connectors, sessions, and encrypted secrets.',
229
- getOperatingGuidance: () => ['Use projects to hold longer-lived goals, objectives, and credential requirements.', 'Create/update tasks for long-lived goals to track progress.', 'Use schedules for follow-ups and heartbeat-style check-ins. Check existing schedules before creating new ones.', 'Inspect existing chats before creating duplicates.'],
229
+ getOperatingGuidance: () => ['Use projects to hold longer-lived goals, objectives, and credential requirements.', 'Create/update tasks for long-lived goals to track progress.', 'When work on a task is finished, update its status to completed with a concrete result summary. Cancel or archive tasks that are no longer relevant. Stale open tasks block project progress.', 'Use schedules for follow-ups and heartbeat-style check-ins. Check existing schedules before creating new ones.', 'Inspect existing chats before creating duplicates.'],
230
230
  } as ExtensionHooks,
231
231
  tools: [
232
232
  {
@@ -5,6 +5,7 @@ import type { Extension, ExtensionHooks } from '@/types'
5
5
  import { registerNativeCapability } from '../native-capabilities'
6
6
  import { normalizeToolInputArgs } from './normalize-tool-args'
7
7
  import { errorMessage, sleep } from '@/lib/shared-utils'
8
+ import { loadAgents } from '@/lib/server/storage'
8
9
  import {
9
10
  cancelDelegationJob,
10
11
  getDelegationJob,
@@ -99,7 +100,12 @@ function validateAllowedSubagentTarget(agentId: string, ctx: ActionContext): str
99
100
  ? ctx.delegationTargetAgentIds.filter((entry): entry is string => typeof entry === 'string' && entry.trim().length > 0)
100
101
  : []
101
102
  if (allowedAgentIds.length === 0 || allowedAgentIds.includes(agentId)) return null
102
- return `Error: agent "${agentId}" is not in the allowed delegate agent list.`
103
+
104
+ const agents = loadAgents()
105
+ const allowedNames = allowedAgentIds
106
+ .map(id => agents[id]?.name ? `${agents[id].name} [${id}]` : id)
107
+ .join(', ')
108
+ return `Error: agent "${agentId}" is not in your allowed delegation list. You may only delegate to: ${allowedNames}. Do not retry with this agent.`
103
109
  }
104
110
 
105
111
  function parseBooleanLike(value: unknown): boolean | unknown {
@@ -538,6 +544,21 @@ registerNativeCapability('subagent', SubagentExtension)
538
544
  */
539
545
  export function buildSubagentTools(bctx: ToolBuildContext): StructuredToolInterface[] {
540
546
  if (!bctx.ctx?.delegationEnabled || !bctx.hasExtension('spawn_subagent')) return []
547
+
548
+ let description = SubagentExtension.tools![0].description
549
+ if (bctx.ctx?.delegationTargetMode === 'selected') {
550
+ const allowedIds = (bctx.ctx.delegationTargetAgentIds || []).filter(
551
+ (id): id is string => typeof id === 'string' && id.trim().length > 0,
552
+ )
553
+ if (allowedIds.length > 0) {
554
+ const agents = loadAgents()
555
+ const allowedSummary = allowedIds
556
+ .map(id => agents[id]?.name ? `${agents[id].name} [${id}]` : id)
557
+ .join(', ')
558
+ description += ` DELEGATION RESTRICTED: You may ONLY delegate to these agents: ${allowedSummary}. Attempts to delegate to any other agent will be rejected.`
559
+ }
560
+ }
561
+
541
562
  return [
542
563
  tool(
543
564
  async (args) => executeSubagentAction(args, {
@@ -548,7 +569,7 @@ export function buildSubagentTools(bctx: ToolBuildContext): StructuredToolInterf
548
569
  }),
549
570
  {
550
571
  name: 'spawn_subagent',
551
- description: SubagentExtension.tools![0].description,
572
+ description,
552
573
  schema: subagentToolSchema
553
574
  }
554
575
  )
@@ -39,6 +39,9 @@ import {
39
39
  simulateSolanaTransaction,
40
40
  } from '../solana'
41
41
  import { TOOL_CAPABILITY } from '../tool-planning'
42
+ import { log } from '@/lib/server/logger'
43
+
44
+ const TAG = 'wallet'
42
45
  import { clearWalletPortfolioCache } from '@/lib/server/wallet/wallet-portfolio'
43
46
  import {
44
47
  createAgentWallet,
@@ -1042,7 +1045,7 @@ async function executeWalletAction(args: unknown, context: { agentId?: string |
1042
1045
  } catch (err: unknown) {
1043
1046
  const msg = errorMessage(err)
1044
1047
  if (msg.includes('429') || msg.toLowerCase().includes('too many requests')) {
1045
- console.warn('[wallet] Solana RPC rate-limited. Consider using a dedicated RPC endpoint (SOLANA_RPC_URL env var).')
1048
+ log.warn(TAG, 'Solana RPC rate-limited. Consider using a dedicated RPC endpoint (SOLANA_RPC_URL env var).')
1046
1049
  }
1047
1050
  return JSON.stringify({ error: msg })
1048
1051
  }
@@ -0,0 +1,85 @@
1
+ import type { Message, Session } from '@/types'
2
+
3
+ import {
4
+ deleteSession as deleteStoredSession,
5
+ disableAllSessionHeartbeats as disableAllStoredSessionHeartbeats,
6
+ getSessionMessages as getStoredSessionMessages,
7
+ loadSession as loadStoredSession,
8
+ loadSessions as loadStoredSessions,
9
+ patchSession as patchStoredSession,
10
+ saveSessions as replaceStoredSessions,
11
+ upsertSession as upsertStoredSession,
12
+ } from '@/lib/server/storage'
13
+ import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
14
+
15
+ export const sessionRepository = createRecordRepository<Session>(
16
+ 'sessions',
17
+ {
18
+ get(id) {
19
+ return loadStoredSession(id) as Session | null
20
+ },
21
+ list() {
22
+ return loadStoredSessions() as Record<string, Session>
23
+ },
24
+ upsert(id, value) {
25
+ upsertStoredSession(id, value as Session)
26
+ },
27
+ upsertMany(entries) {
28
+ replaceStoredSessions(Object.fromEntries(entries))
29
+ },
30
+ replace(data) {
31
+ replaceStoredSessions(data)
32
+ },
33
+ patch(id, updater) {
34
+ return patchStoredSession(id, updater as (current: Session | null) => Session | null) as Session | null
35
+ },
36
+ delete(id) {
37
+ deleteStoredSession(id)
38
+ },
39
+ },
40
+ )
41
+
42
+ export function listSessions(): Record<string, Session> {
43
+ return sessionRepository.list()
44
+ }
45
+
46
+ export function getSession(id: string): Session | null {
47
+ return sessionRepository.get(id)
48
+ }
49
+
50
+ export function getSessions(ids: string[]): Record<string, Session> {
51
+ return sessionRepository.getMany(ids)
52
+ }
53
+
54
+ export function saveSession(id: string, session: Session | Record<string, unknown>): void {
55
+ sessionRepository.upsert(id, session as Session)
56
+ }
57
+
58
+ export function saveSessionMany(entries: Array<[string, Session | Record<string, unknown>]>): void {
59
+ sessionRepository.upsertMany(entries as Array<[string, Session]>)
60
+ }
61
+
62
+ export function replaceSessions(sessions: Record<string, Session | Record<string, unknown>>): void {
63
+ sessionRepository.replace(sessions as Record<string, Session>)
64
+ }
65
+
66
+ export function patchSession(id: string, updater: (current: Session | null) => Session | null): Session | null {
67
+ return sessionRepository.patch(id, updater)
68
+ }
69
+
70
+ export function deleteSession(id: string): void {
71
+ sessionRepository.delete(id)
72
+ }
73
+
74
+ export function getSessionMessages(sessionId: string): Message[] {
75
+ return getStoredSessionMessages(sessionId)
76
+ }
77
+
78
+ export function disableAllSessionHeartbeats(): number {
79
+ return disableAllStoredSessionHeartbeats()
80
+ }
81
+
82
+ export const loadSessions = listSessions
83
+ export const loadSession = getSession
84
+ export const saveSessions = replaceSessions
85
+ export const upsertSession = saveSession
@@ -0,0 +1,25 @@
1
+ import type { AppSettings } from '@/types'
2
+
3
+ import {
4
+ loadPublicSettings as loadStoredPublicSettings,
5
+ loadSettings as loadStoredSettings,
6
+ saveSettings as saveStoredSettings,
7
+ } from '@/lib/server/storage'
8
+ import { createSingletonRepository } from '@/lib/server/persistence/repository-utils'
9
+
10
+ export const settingsRepository = createSingletonRepository<AppSettings>(
11
+ 'settings',
12
+ {
13
+ get() {
14
+ return loadStoredSettings()
15
+ },
16
+ save(value) {
17
+ saveStoredSettings(value)
18
+ },
19
+ },
20
+ )
21
+
22
+ export const loadSettings = () => settingsRepository.get()
23
+ export const saveSettings = (value: AppSettings | Record<string, unknown>) => settingsRepository.save(value as AppSettings)
24
+ export const patchSettings = (updater: (current: AppSettings) => AppSettings | Record<string, unknown>) => settingsRepository.patch(updater as (current: AppSettings) => AppSettings)
25
+ export const loadPublicSettings = () => loadStoredPublicSettings()
@@ -1,5 +1,8 @@
1
1
  import type { ClawHubSkill } from '@/types'
2
2
  import { errorMessage } from '@/lib/shared-utils'
3
+ import { log } from '@/lib/server/logger'
4
+
5
+ const TAG = 'clawhub-client'
3
6
 
4
7
  export interface ClawHubSearchResult {
5
8
  skills: ClawHubSkill[]
@@ -177,7 +180,7 @@ export async function searchClawHub(query: string, page = 1, limit = 20, cursor?
177
180
  return { skills, total, page, nextCursor: data.nextCursor }
178
181
  } catch (err: unknown) {
179
182
  const error = errorMessage(err)
180
- console.warn('[clawhub] search failed:', error)
183
+ log.warn(TAG, 'search failed:', error)
181
184
  return { skills: [], total: 0, page, error }
182
185
  }
183
186
  }
@@ -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
  }
@@ -5,14 +5,14 @@ import path from 'node:path'
5
5
  import test from 'node:test'
6
6
  import { clearDiscoveredSkillsCache, discoverSkills } from './skill-discovery'
7
7
 
8
- test('discoverSkills includes tracked bundled skills from bundled-skills', () => {
8
+ test('discoverSkills includes tracked bundled skills from skills', () => {
9
9
  const skills = discoverSkills({ cwd: path.join(process.cwd(), 'src') })
10
10
  const googleWorkspaceSkill = skills.find((skill) => skill.name === 'google-workspace')
11
11
 
12
12
  assert.ok(googleWorkspaceSkill)
13
13
  assert.equal(googleWorkspaceSkill?.source, 'bundled')
14
14
  assert.equal(
15
- googleWorkspaceSkill?.sourcePath.endsWith(path.join('bundled-skills', 'google-workspace', 'SKILL.md')),
15
+ googleWorkspaceSkill?.sourcePath.endsWith(path.join('skills', 'google-workspace', 'SKILL.md')),
16
16
  true,
17
17
  )
18
18
  })
@@ -19,7 +19,7 @@ interface DiscoveryCache {
19
19
  }
20
20
 
21
21
  const CACHE_TTL_MS = 5_000
22
- const BUNDLED_SKILLS_DIR = path.join(process.cwd(), 'bundled-skills')
22
+ const BUNDLED_SKILLS_DIR = path.join(process.cwd(), 'skills')
23
23
  const LEGACY_BUNDLED_SKILLS_DIR = path.join(DATA_DIR, 'skills')
24
24
 
25
25
  let cache: DiscoveryCache | null = null
@@ -84,7 +84,7 @@ function scanLayer(
84
84
 
85
85
  /**
86
86
  * Discover skills from three layers:
87
- * 1. Bundled: `bundled-skills/` (tracked with the app)
87
+ * 1. Bundled: `skills/` (tracked with the app)
88
88
  * Legacy fallback: `data/skills/`
89
89
  * 2. Workspace: `<swarmclaw-home>/skills/` (user-installed)
90
90
  * 3. Project: `<cwd>/skills/` (project-local)
@@ -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)
@@ -0,0 +1,14 @@
1
+ export {
2
+ deleteLearnedSkill,
3
+ deleteSkill,
4
+ loadLearnedSkill,
5
+ loadLearnedSkills,
6
+ loadSkillSuggestions,
7
+ loadSkills,
8
+ patchLearnedSkill,
9
+ patchSkillSuggestion,
10
+ saveLearnedSkills,
11
+ saveSkills,
12
+ upsertLearnedSkill,
13
+ upsertSkillSuggestion,
14
+ } from '@/lib/server/storage'
@@ -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
  }