@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,9 +1,12 @@
1
+ import { log } from '@/lib/server/logger'
1
2
  import { spawn, execSync } from 'child_process'
2
3
  import type { ChildProcess } from 'child_process'
3
4
  import type { Connector } from '@/types'
4
5
  import type { PlatformConnector, ConnectorInstance, InboundMessage, ConnectorIngressResult } from './types'
5
6
  import { resolveConnectorIngressReply } from './ingress-delivery'
6
7
 
8
+ const TAG = 'signal'
9
+
7
10
  const signal: PlatformConnector = {
8
11
  async start(connector, _botToken, onMessage): Promise<ConnectorInstance> {
9
12
  const phoneNumber = connector.config.phoneNumber
@@ -43,16 +46,16 @@ const signal: PlatformConnector = {
43
46
 
44
47
  daemonProc.stderr?.on('data', (chunk: Buffer) => {
45
48
  const msg = chunk.toString().trim()
46
- if (msg) console.error(`[signal] stderr: ${msg}`)
49
+ if (msg) log.error(TAG, `stderr: ${msg}`)
47
50
  })
48
51
 
49
52
  daemonProc.on('exit', (code) => {
50
53
  if (!stopped) {
51
- console.error(`[signal] daemon exited unexpectedly with code ${code}`)
54
+ log.error(TAG, `daemon exited unexpectedly with code ${code}`)
52
55
  }
53
56
  })
54
57
 
55
- console.log(`[signal] Daemon started in stdio mode for ${phoneNumber}`)
58
+ log.info(TAG, `Daemon started in stdio mode for ${phoneNumber}`)
56
59
  } else if (mode === 'http') {
57
60
  // Poll the signal-cli REST API for incoming messages
58
61
  const pollInterval = 2000
@@ -74,7 +77,7 @@ const signal: PlatformConnector = {
74
77
  }
75
78
 
76
79
  pollTimer = setInterval(poll, pollInterval)
77
- console.log(`[signal] Polling ${httpUrl} for ${phoneNumber} every ${pollInterval}ms`)
80
+ log.info(TAG, `Polling ${httpUrl} for ${phoneNumber} every ${pollInterval}ms`)
78
81
  } else {
79
82
  throw new Error(`Unknown signalCliMode: ${mode}. Use 'stdio' or 'http'.`)
80
83
  }
@@ -119,7 +122,7 @@ const signal: PlatformConnector = {
119
122
  daemonProc.kill('SIGTERM')
120
123
  daemonProc = null
121
124
  }
122
- console.log(`[signal] Connector stopped for ${phoneNumber}`)
125
+ log.info(TAG, `Connector stopped for ${phoneNumber}`)
123
126
  },
124
127
  }
125
128
  },
@@ -177,7 +180,7 @@ export async function handleSignalEvent(
177
180
  )
178
181
  }
179
182
  } catch (err: any) {
180
- console.error(`[signal] Error handling message:`, err.message)
183
+ log.error(TAG, 'Error handling message:', err.message)
181
184
  }
182
185
  }
183
186
 
@@ -7,6 +7,9 @@ import { resolveConnectorIngressReply } from './ingress-delivery'
7
7
  import { deliverChunkedConnectorText } from './delivery'
8
8
  import { downloadInboundMediaToUpload, inferInboundMediaType, mimeFromPath, isImageMime } from './media'
9
9
  import { dedup, errorMessage } from '@/lib/shared-utils'
10
+ import { log } from '@/lib/server/logger'
11
+
12
+ const TAG = 'slack'
10
13
 
11
14
  function normalizeSlackEmoji(input: string): string {
12
15
  const raw = input.trim().replace(/^:|:$/g, '')
@@ -99,7 +102,7 @@ async function hydrateSlackThreadContext(params: {
99
102
  params.inbound.threadPersonaLabel = params.inbound.threadTitle
100
103
  params.inbound.threadHistory = history.length ? history : undefined
101
104
  } catch (err: unknown) {
102
- console.warn(`[slack] Thread context bootstrap failed: ${errorMessage(err)}`)
105
+ log.warn(TAG, `Thread context bootstrap failed: ${errorMessage(err)}`)
103
106
  }
104
107
  }
105
108
 
@@ -141,7 +144,7 @@ const slack: PlatformConnector = {
141
144
  throw new Error('Auth test returned empty — the bot token may be revoked or the app uninstalled')
142
145
  }
143
146
  botUserId = auth.user_id as string
144
- console.log(`[slack] Authenticated as @${auth.user} in workspace "${auth.team}"`)
147
+ log.info(TAG, `Authenticated as @${auth.user} in workspace "${auth.team}"`)
145
148
  } catch (err: any) {
146
149
  const hint = err.code === 'slack_webapi_platform_error'
147
150
  ? '. Check that your Bot Token (xoxb-...) is correct and the app is installed to the workspace.'
@@ -159,7 +162,7 @@ const slack: PlatformConnector = {
159
162
 
160
163
  // Catch global errors so they don't become unhandled rejections
161
164
  app.error(async (error) => {
162
- console.error(`[slack] App error:`, error)
165
+ log.error(TAG, 'App error:', error)
163
166
  })
164
167
 
165
168
  // Optional: restrict to specific channels
@@ -177,7 +180,7 @@ const slack: PlatformConnector = {
177
180
  const channelId = msg.channel
178
181
  if (allowedChannels && !allowedChannels.includes(channelId)) return
179
182
 
180
- console.log(`[slack] Message in ${channelId} from ${msg.user}: ${(msg.text || '').slice(0, 80)}`)
183
+ log.info(TAG, `Message in ${channelId} from ${msg.user}: ${(msg.text || '').slice(0, 80)}`)
181
184
 
182
185
  // Get user info for display name
183
186
  let senderName = msg.user || 'unknown'
@@ -213,7 +216,7 @@ const slack: PlatformConnector = {
213
216
  continue
214
217
  }
215
218
  } catch (err: any) {
216
- console.warn(`[slack] Media download failed (${f?.name || 'file'}):`, err?.message || String(err))
219
+ log.warn(TAG, `Media download failed (${f?.name || 'file'}):`, err?.message || String(err))
217
220
  }
218
221
  }
219
222
  media.push({
@@ -262,7 +265,7 @@ const slack: PlatformConnector = {
262
265
  },
263
266
  })
264
267
  } catch (err: any) {
265
- console.error(`[slack] Error handling message:`, err.message)
268
+ log.error(TAG, 'Error handling message:', err.message)
266
269
  try {
267
270
  await say('Sorry, I encountered an error processing your message.')
268
271
  } catch { /* ignore */ }
@@ -320,12 +323,12 @@ const slack: PlatformConnector = {
320
323
  },
321
324
  })
322
325
  } catch (err: any) {
323
- console.error(`[slack] Error handling mention:`, err.message)
326
+ log.error(TAG, 'Error handling mention:', err.message)
324
327
  }
325
328
  })
326
329
 
327
330
  await app.start()
328
- console.log(`[slack] Bot connected (socket mode)`)
331
+ log.info(TAG, 'Bot connected (socket mode)')
329
332
 
330
333
  let appStopped = false
331
334
 
@@ -423,14 +426,14 @@ const slack: PlatformConnector = {
423
426
  async stop() {
424
427
  appStopped = true
425
428
  await app.stop()
426
- console.log(`[slack] Bot disconnected`)
429
+ log.info(TAG, 'Bot disconnected')
427
430
  },
428
431
  }
429
432
 
430
433
  // Bolt emits 'error' on unrecoverable failures (auth revoked, socket closed permanently)
431
434
  app.error(async (error) => {
432
435
  const errMsg = error.original?.message || error.message || String(error)
433
- console.error(`[slack] App error:`, errMsg)
436
+ log.error(TAG, 'App error:', errMsg)
434
437
  if (appStopped) return
435
438
  appStopped = true
436
439
  instance.onCrash?.(`Slack error: ${errMsg}`)
@@ -1,6 +1,9 @@
1
+ import { log } from '@/lib/server/logger'
1
2
  import type { PlatformConnector, ConnectorInstance, InboundMessage } from './types'
2
3
  import { resolveConnectorIngressReply } from './ingress-delivery'
3
4
 
5
+ const TAG = 'teams'
6
+
4
7
  const teams: PlatformConnector = {
5
8
  async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
6
9
  const pkg = 'botbuilder'
@@ -15,7 +18,7 @@ const teams: PlatformConnector = {
15
18
  })
16
19
 
17
20
  adapter.onTurnError = async (_context: unknown, error: Error) => {
18
- console.error(`[teams] Turn error:`, error.message)
21
+ log.error(TAG, 'Turn error:', error.message)
19
22
  }
20
23
 
21
24
  // Store conversation references for proactive messaging
@@ -49,7 +52,7 @@ const teams: PlatformConnector = {
49
52
  if (!reply) return
50
53
  await context.sendActivity(reply.visibleText)
51
54
  } catch (err: any) {
52
- console.error(`[teams] Error handling message:`, err.message)
55
+ log.error(TAG, 'Error handling message:', err.message)
53
56
  try {
54
57
  await context.sendActivity('Sorry, I encountered an error processing your message.')
55
58
  } catch { /* ignore */ }
@@ -61,8 +64,8 @@ const teams: PlatformConnector = {
61
64
  const handlerKey = `__swarmclaw_teams_handler_${connector.id}__`
62
65
  ;(globalThis as any)[handlerKey] = processActivity
63
66
 
64
- console.log(`[teams] Bot registered (appId: ${appId})`)
65
- console.log(`[teams] Configure your bot's messaging endpoint to POST to /api/connectors/${connector.id}/webhook`)
67
+ log.info(TAG, `Bot registered (appId: ${appId})`)
68
+ log.info(TAG, `Configure your bot's messaging endpoint to POST to /api/connectors/${connector.id}/webhook`)
66
69
 
67
70
  return {
68
71
  connector,
@@ -85,7 +88,7 @@ const teams: PlatformConnector = {
85
88
  stopped = true
86
89
  delete (globalThis as any)[handlerKey]
87
90
  conversationReferences.clear()
88
- console.log(`[teams] Bot disconnected`)
91
+ log.info(TAG, 'Bot disconnected')
89
92
  },
90
93
  }
91
94
  },
@@ -7,6 +7,9 @@ import { resolveConnectorIngressReply } from './ingress-delivery'
7
7
  import { deliverChunkedConnectorText } from './delivery'
8
8
  import { downloadInboundMediaToUpload, inferInboundMediaType, mimeFromPath, isImageMime, isAudioMime } from './media'
9
9
  import { errorMessage } from '@/lib/shared-utils'
10
+ import { log } from '@/lib/server/logger'
11
+
12
+ const TAG = 'telegram'
10
13
 
11
14
  const telegram: PlatformConnector = {
12
15
  async start(connector, botToken, onMessage): Promise<ConnectorInstance> {
@@ -20,23 +23,23 @@ const telegram: PlatformConnector = {
20
23
 
21
24
  // Log all errors
22
25
  bot.catch((err) => {
23
- console.error(`[telegram] Bot error:`, err.message || err)
26
+ log.error(TAG, 'Bot error:', err.message || err)
24
27
  })
25
28
 
26
29
  // Delete any existing webhook so long polling works
27
30
  await bot.api.deleteWebhook().catch((err) => {
28
- console.error('[telegram] Failed to delete webhook:', err.message)
31
+ log.error(TAG, 'Failed to delete webhook:', err.message)
29
32
  })
30
33
 
31
34
  // Log all incoming updates for debugging
32
35
  bot.use(async (ctx, next) => {
33
- console.log(`[telegram] Update received: chat=${ctx.chat?.id}, from=${ctx.from?.first_name}, hasText=${!!ctx.message?.text}`)
36
+ log.info(TAG, `Update received: chat=${ctx.chat?.id}, from=${ctx.from?.first_name}, hasText=${!!ctx.message?.text}`)
34
37
  await next()
35
38
  })
36
39
 
37
40
  // Handle /start command (required for new conversations)
38
41
  bot.command('start', async (ctx) => {
39
- console.log(`[telegram] /start from ${ctx.from?.first_name} (chat=${ctx.chat.id})`)
42
+ log.info(TAG, `/start from ${ctx.from?.first_name} (chat=${ctx.chat.id})`)
40
43
  await ctx.reply('Hello! I\'m ready to chat. Send me a message.')
41
44
  })
42
45
 
@@ -51,15 +54,15 @@ const telegram: PlatformConnector = {
51
54
  // that appear as short bracketed strings like [rr], [e], [read] etc.
52
55
  const hasMedia = raw.photo || raw.video || raw.audio || raw.voice || raw.document || raw.animation
53
56
  if (!hasMedia && /^\[.{1,5}\]$/.test(text.trim())) {
54
- console.log(`[telegram] Ignoring system event from ${ctx.from.first_name}: ${text}`)
57
+ log.info(TAG, `Ignoring system event from ${ctx.from.first_name}: ${text}`)
55
58
  return
56
59
  }
57
60
 
58
- console.log(`[telegram] Message from ${ctx.from.first_name} (chat=${chatId}): ${String(text).slice(0, 80)}`)
61
+ log.info(TAG, `Message from ${ctx.from.first_name} (chat=${chatId}): ${String(text).slice(0, 80)}`)
59
62
 
60
63
  // Filter by allowed chats if configured
61
64
  if (allowedChats && !allowedChats.includes(chatId)) {
62
- console.log(`[telegram] Skipping — chat ${chatId} not in allowed list: ${allowedChats.join(',')}`)
65
+ log.info(TAG, `Skipping — chat ${chatId} not in allowed list: ${allowedChats.join(',')}`)
63
66
  return
64
67
  }
65
68
 
@@ -125,7 +128,7 @@ const telegram: PlatformConnector = {
125
128
  })
126
129
  if (stored) media.push(stored)
127
130
  } catch (err: any) {
128
- console.warn(`[telegram] Failed to fetch media ${m.fileId}:`, err?.message || String(err))
131
+ log.warn(TAG, `Failed to fetch media ${m.fileId}:`, err?.message || String(err))
129
132
  media.push({
130
133
  type: m.type,
131
134
  fileName: m.fileName,
@@ -175,7 +178,7 @@ const telegram: PlatformConnector = {
175
178
  },
176
179
  })
177
180
  } catch (err: any) {
178
- console.error(`[telegram] Error handling message:`, err.message)
181
+ log.error(TAG, 'Error handling message:', err.message)
179
182
  try {
180
183
  await ctx.reply('Sorry, I encountered an error processing your message.')
181
184
  } catch { /* ignore */ }
@@ -268,7 +271,7 @@ const telegram: PlatformConnector = {
268
271
  async stop() {
269
272
  botRunning = false
270
273
  await bot.stop()
271
- console.log(`[telegram] Bot stopped`)
274
+ log.info(TAG, 'Bot stopped')
272
275
  },
273
276
  }
274
277
 
@@ -277,12 +280,12 @@ const telegram: PlatformConnector = {
277
280
  allowed_updates: ['message', 'edited_message'],
278
281
  onStart: (botInfo) => {
279
282
  botUsername = botInfo.username || ''
280
- console.log(`[telegram] Bot started as @${botInfo.username} — polling for updates`)
283
+ log.info(TAG, `Bot started as @${botInfo.username} — polling for updates`)
281
284
  },
282
285
  }).catch((err: unknown) => {
283
286
  botRunning = false
284
287
  const errMsg = errorMessage(err)
285
- console.error(`[telegram] Polling stopped with error:`, errMsg)
288
+ log.error(TAG, 'Polling stopped with error:', errMsg)
286
289
  instance.onCrash?.(`Polling stopped: ${errMsg}`)
287
290
  })
288
291
 
@@ -1,5 +1,5 @@
1
1
  import makeWASocket, {
2
- useMultiFileAuthState,
2
+ useMultiFileAuthState as createMultiFileAuthState,
3
3
  DisconnectReason,
4
4
  fetchLatestBaileysVersion,
5
5
  normalizeMessageContent,
@@ -22,6 +22,9 @@ import { getWhatsAppApprovedSenderIds } from './pairing'
22
22
 
23
23
  import { DATA_DIR } from '../data-dir'
24
24
  import { loadConnectors, loadSettings } from '../storage'
25
+ import { log } from '@/lib/server/logger'
26
+
27
+ const TAG = 'whatsapp'
25
28
 
26
29
  const AUTH_DIR = path.join(DATA_DIR, 'whatsapp-auth')
27
30
  const INBOUND_DEDUPE_TTL_MS = 2 * 60 * 1000
@@ -210,7 +213,7 @@ function transcodeToWhatsAppVoiceNote(params: {
210
213
  })
211
214
  if ((result.status ?? 1) !== 0 || !fs.existsSync(outputPath)) {
212
215
  const stderr = (result.stderr || '').trim()
213
- console.warn(`[whatsapp] Failed to transcode voice note to opus/ogg${stderr ? `: ${stderr}` : ''}`)
216
+ log.warn(TAG, `Failed to transcode voice note to opus/ogg${stderr ? `: ${stderr}` : ''}`)
214
217
  return null
215
218
  }
216
219
  return {
@@ -396,7 +399,7 @@ export function clearAuthDir(connectorId: string): void {
396
399
  const authDir = path.join(AUTH_DIR, connectorId)
397
400
  if (fs.existsSync(authDir)) {
398
401
  fs.rmSync(authDir, { recursive: true, force: true })
399
- console.log(`[whatsapp] Cleared auth state for connector ${connectorId}`)
402
+ log.info(TAG, `Cleared auth state for connector ${connectorId}`)
400
403
  }
401
404
  }
402
405
 
@@ -406,7 +409,7 @@ const whatsapp: PlatformConnector = {
406
409
  const authDir = path.join(AUTH_DIR, connector.id)
407
410
  if (!fs.existsSync(authDir)) fs.mkdirSync(authDir, { recursive: true })
408
411
 
409
- const { state, saveCreds } = await useMultiFileAuthState(authDir)
412
+ const { state, saveCreds } = await createMultiFileAuthState(authDir)
410
413
  const { version } = await fetchLatestBaileysVersion()
411
414
 
412
415
  let sock: ReturnType<typeof makeWASocket> | null = null
@@ -463,7 +466,7 @@ const whatsapp: PlatformConnector = {
463
466
  sent = await sock.sendMessage(channelId, { image: buf, caption, mimetype: mime })
464
467
  } catch (err: unknown) {
465
468
  const errMsg = errorMessage(err)
466
- console.warn(`[whatsapp] Image send failed (${errMsg}); retrying as document: ${fName}`)
469
+ log.warn(TAG, `Image send failed (${errMsg}); retrying as document: ${fName}`)
467
470
  sent = await sock.sendMessage(channelId, { document: buf, fileName: fName, mimetype: mime, caption })
468
471
  }
469
472
  } else if (isAudioMime(mime)) {
@@ -523,7 +526,7 @@ const whatsapp: PlatformConnector = {
523
526
  clearReconnectTimer()
524
527
  try { sock?.end(undefined) } catch { /* ignore */ }
525
528
  sock = null
526
- console.log(`[whatsapp] Stopped connector: ${connector.name}`)
529
+ log.info(TAG, `Stopped connector: ${connector.name}`)
527
530
  },
528
531
  }
529
532
 
@@ -545,7 +548,7 @@ const whatsapp: PlatformConnector = {
545
548
 
546
549
  const gen = ++socketGen // Capture generation for stale detection
547
550
  connectionState = 'connecting'
548
- console.log(`[whatsapp] Starting socket gen=${gen} for ${connector.name} (hasCreds=${instance.hasCredentials})`)
551
+ log.info(TAG, `Starting socket gen=${gen} for ${connector.name} (hasCreds=${instance.hasCredentials})`)
549
552
 
550
553
  sock = makeWASocket({
551
554
  version,
@@ -564,10 +567,10 @@ const whatsapp: PlatformConnector = {
564
567
 
565
568
  const { connection, lastDisconnect, qr } = update
566
569
  if (typeof connection === 'string' && connection) connectionState = connection
567
- console.log(`[whatsapp] Connection update gen=${gen}: connection=${connection}, hasQR=${!!qr}`)
570
+ log.info(TAG, `Connection update gen=${gen}: connection=${connection}, hasQR=${!!qr}`)
568
571
 
569
572
  if (qr) {
570
- console.log(`[whatsapp] QR code generated for ${connector.name}`)
573
+ log.info(TAG, `QR code generated for ${connector.name}`)
571
574
  try {
572
575
  instance.qrDataUrl = await QRCode.toDataURL(qr, {
573
576
  width: 280,
@@ -575,17 +578,17 @@ const whatsapp: PlatformConnector = {
575
578
  color: { dark: '#000000', light: '#ffffff' },
576
579
  })
577
580
  } catch (err) {
578
- console.error('[whatsapp] Failed to generate QR data URL:', err)
581
+ log.error(TAG, 'Failed to generate QR data URL:', err)
579
582
  }
580
583
  }
581
584
  if (connection === 'close') {
582
585
  instance.qrDataUrl = null
583
586
  const reason = (lastDisconnect?.error as any)?.output?.statusCode
584
- console.log(`[whatsapp] Connection closed: reason=${reason} stopped=${stopped}`)
587
+ log.info(TAG, `Connection closed: reason=${reason} stopped=${stopped}`)
585
588
 
586
589
  if (reason === DisconnectReason.loggedOut) {
587
590
  // Session invalidated — clear auth and restart to get fresh QR
588
- console.log(`[whatsapp] Logged out — clearing auth and restarting for fresh QR`)
591
+ log.info(TAG, `Logged out — clearing auth and restarting for fresh QR`)
589
592
  instance.authenticated = false
590
593
  instance.hasCredentials = false
591
594
  clearAuthDir(connector.id)
@@ -597,38 +600,38 @@ const whatsapp: PlatformConnector = {
597
600
  } else if (reason === 440) {
598
601
  // Conflict — another session replaced this one. Do NOT reconnect
599
602
  // (reconnecting would create a ping-pong loop with the other session)
600
- console.log(`[whatsapp] Session conflict (replaced by another connection) — stopping`)
603
+ log.info(TAG, `Session conflict (replaced by another connection) — stopping`)
601
604
  instance.authenticated = false
602
605
  instance.onCrash?.('Session conflict — replaced by another connection')
603
606
  } else if (!stopped) {
604
- console.log(`[whatsapp] Reconnecting in 3s...`)
607
+ log.info(TAG, `Reconnecting in 3s...`)
605
608
  scheduleReconnect(3000)
606
609
  } else {
607
- console.log(`[whatsapp] Disconnected permanently`)
610
+ log.info(TAG, `Disconnected permanently`)
608
611
  }
609
612
  } else if (connection === 'open') {
610
613
  instance.authenticated = true
611
614
  instance.hasCredentials = true
612
615
  instance.qrDataUrl = null
613
- console.log(`[whatsapp] Connected as ${sock?.user?.id}`)
616
+ log.info(TAG, `Connected as ${sock?.user?.id}`)
614
617
  }
615
618
  })
616
619
 
617
620
  sock.ev.on('messages.upsert', async (upsert) => {
618
621
  const { messages, type } = upsert
619
- console.log(`[whatsapp] messages.upsert gen=${gen}: type=${type}, count=${messages.length}`)
622
+ log.info(TAG, `messages.upsert gen=${gen}: type=${type}, count=${messages.length}`)
620
623
 
621
624
  if (gen !== socketGen) {
622
- console.log(`[whatsapp] Ignoring stale socket event (gen=${gen}, current=${socketGen})`)
625
+ log.info(TAG, `Ignoring stale socket event (gen=${gen}, current=${socketGen})`)
623
626
  return
624
627
  }
625
628
  if (type !== 'notify') {
626
- console.log(`[whatsapp] Ignoring non-notify upsert type: ${type}`)
629
+ log.info(TAG, `Ignoring non-notify upsert type: ${type}`)
627
630
  return
628
631
  }
629
632
 
630
633
  for (const msg of messages) {
631
- console.log(`[whatsapp] Processing message: fromMe=${msg.key.fromMe}, jid=${msg.key.remoteJid}, hasConversation=${!!msg.message?.conversation}, hasExtended=${!!msg.message?.extendedTextMessage}`)
634
+ log.info(TAG, `Processing message: fromMe=${msg.key.fromMe}, jid=${msg.key.remoteJid}, hasConversation=${!!msg.message?.conversation}, hasExtended=${!!msg.message?.extendedTextMessage}`)
632
635
 
633
636
  if (msg.key.remoteJid === 'status@broadcast') continue
634
637
 
@@ -637,7 +640,7 @@ const whatsapp: PlatformConnector = {
637
640
  const now = Date.now()
638
641
  const seenAt = seenInboundMessageIds.get(msgId)
639
642
  if (typeof seenAt === 'number' && now - seenAt <= INBOUND_DEDUPE_TTL_MS) {
640
- console.log(`[whatsapp] Skipping duplicate inbound message id: ${msgId}`)
643
+ log.info(TAG, `Skipping duplicate inbound message id: ${msgId}`)
641
644
  continue
642
645
  }
643
646
  seenInboundMessageIds.set(msgId, now)
@@ -650,7 +653,7 @@ const whatsapp: PlatformConnector = {
650
653
 
651
654
  // Skip messages sent by the bot itself (tracked by ID to prevent infinite loops)
652
655
  if (msg.key.id && sentMessageIds.has(msg.key.id)) {
653
- console.log(`[whatsapp] Skipping own bot reply: ${msg.key.id}`)
656
+ log.info(TAG, `Skipping own bot reply: ${msg.key.id}`)
654
657
  sentMessageIds.delete(msg.key.id) // Clean up
655
658
  continue
656
659
  }
@@ -662,7 +665,7 @@ const whatsapp: PlatformConnector = {
662
665
  const myPhoneNum = sock?.user?.id?.split(':')[0] || ''
663
666
  const myLid = sock?.user?.lid?.split(':')[0] || ''
664
667
  const isSelfChat = (remoteNum === myPhoneNum) || (remoteHost === 'lid' && (myLid ? remoteNum === myLid : true))
665
- console.log(`[whatsapp] Self-chat check: remote=${remoteNum}@${remoteHost}, myPhone=${myPhoneNum}, myLid=${myLid}, isSelf=${isSelfChat}`)
668
+ log.info(TAG, `Self-chat check: remote=${remoteNum}@${remoteHost}, myPhone=${myPhoneNum}, myLid=${myLid}, isSelf=${isSelfChat}`)
666
669
  if (msg.key.fromMe && !isSelfChat) continue
667
670
 
668
671
  const jid = msg.key.remoteJid || ''
@@ -676,9 +679,9 @@ const whatsapp: PlatformConnector = {
676
679
  // Self-chat always passes the filter (it's the bot's own account)
677
680
  if (allowedJids?.length && !isSelfChat) {
678
681
  const matched = isWhatsAppInboundAllowed({ allowedJids, msg, isSelfChat })
679
- console.log(`[whatsapp] JID filter: candidates=${collectWhatsAppAddressCandidates(msg).join(',')}, allowedJids=${allowedJids.join(',')}, matched=${matched}`)
682
+ log.info(TAG, `JID filter: candidates=${collectWhatsAppAddressCandidates(msg).join(',')}, allowedJids=${allowedJids.join(',')}, matched=${matched}`)
680
683
  if (!matched) {
681
- console.log(`[whatsapp] Skipping message from non-allowed JID: ${jid}`)
684
+ log.info(TAG, `Skipping message from non-allowed JID: ${jid}`)
682
685
  continue
683
686
  }
684
687
  }
@@ -712,7 +715,7 @@ const whatsapp: PlatformConnector = {
712
715
  })
713
716
  media.push(saved)
714
717
  } catch (err: any) {
715
- console.error(`[whatsapp] Failed to decode media: ${err?.message || String(err)}`)
718
+ log.error(TAG, `Failed to decode media: ${err?.message || String(err)}`)
716
719
  media.push({
717
720
  type: mediaCandidate.kind,
718
721
  fileName: mediaCandidate.payload?.fileName || undefined,
@@ -736,7 +739,7 @@ const whatsapp: PlatformConnector = {
736
739
  }) || undefined
737
740
  }
738
741
 
739
- console.log(`[whatsapp] Message from ${inbound.senderName} (${jid}): ${inbound.text.slice(0, 80)}`)
742
+ log.info(TAG, `Message from ${inbound.senderName} (${jid}): ${inbound.text.slice(0, 80)}`)
740
743
 
741
744
  try {
742
745
  const reply = await resolveConnectorIngressReply(onMessage, inbound)
@@ -758,10 +761,10 @@ const whatsapp: PlatformConnector = {
758
761
  state: 'sent',
759
762
  })
760
763
  } catch (recordErr: unknown) {
761
- console.warn(`[whatsapp] Delivery recording failed (response already sent):`, errorMessage(recordErr))
764
+ log.warn(TAG, 'Delivery recording failed (response already sent):', errorMessage(recordErr))
762
765
  }
763
766
  } catch (err: unknown) {
764
- console.error(`[whatsapp] Error handling message:`, errorMessage(err))
767
+ log.error(TAG, 'Error handling message:', errorMessage(err))
765
768
  try {
766
769
  await sock!.sendMessage(jid, { text: 'Sorry, I encountered an error processing your message.' })
767
770
  } catch { /* ignore */ }
@@ -0,0 +1,7 @@
1
+ export {
2
+ decryptKey,
3
+ deleteCredential,
4
+ encryptKey,
5
+ loadCredentials,
6
+ saveCredentials,
7
+ } from '@/lib/server/storage'
@@ -1,5 +1,8 @@
1
1
  import { loadSettings, loadCredentials, decryptKey } from './storage'
2
2
  import { hmrSingleton } from '@/lib/shared-utils'
3
+ import { log } from '@/lib/server/logger'
4
+
5
+ const TAG = 'embeddings'
3
6
 
4
7
  interface PipelineState {
5
8
  instance: unknown
@@ -62,7 +65,7 @@ export async function getEmbedding(text: string): Promise<number[] | null> {
62
65
  return await ollamaEmbed(text, model, settings.embeddingEndpoint)
63
66
  }
64
67
  } catch (err: unknown) {
65
- console.error(`[embeddings] Error computing embedding:`, err instanceof Error ? err.message : String(err))
68
+ log.error(TAG, 'Error computing embedding:', err instanceof Error ? err.message : String(err))
66
69
  }
67
70
 
68
71
  return null
@@ -0,0 +1,4 @@
1
+ export {
2
+ loadGatewayProfiles,
3
+ saveGatewayProfiles,
4
+ } from '@/lib/server/storage'
@@ -1,6 +1,9 @@
1
1
  import * as cheerio from 'cheerio'
2
2
  import { dedup } from '@/lib/shared-utils'
3
3
  import { truncate } from './session-tools/context'
4
+ import { log } from '@/lib/server/logger'
5
+
6
+ const TAG = 'link-understanding'
4
7
 
5
8
  const BARE_LINK_RE = /https?:\/\/\S+/gi
6
9
 
@@ -48,7 +51,7 @@ export async function runLinkUnderstanding(message: string): Promise<string[]> {
48
51
  }
49
52
  } catch (err) {
50
53
  // Fail silently for link understanding — don't block the main run
51
- console.error(`Link understanding failed for ${url}:`, err)
54
+ log.error(TAG, `Link understanding failed for ${url}:`, err)
52
55
  }
53
56
  }
54
57
 
@@ -0,0 +1,59 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { generateAbstract } from './memory-abstract'
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Short content guard
8
+ // ---------------------------------------------------------------------------
9
+
10
+ describe('generateAbstract', () => {
11
+ it('returns null for content <= 200 chars', async () => {
12
+ const short = 'A'.repeat(200)
13
+ assert.equal(await generateAbstract(short, 'title'), null)
14
+ })
15
+
16
+ it('returns null for empty content', async () => {
17
+ assert.equal(await generateAbstract('', 'title'), null)
18
+ })
19
+
20
+ it('returns null for content exactly 200 chars', async () => {
21
+ assert.equal(await generateAbstract('x'.repeat(200)), null)
22
+ })
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Fallback abstract
26
+ // ---------------------------------------------------------------------------
27
+
28
+ it('returns fallback (truncated prefix) when LLM import fails', async () => {
29
+ // generateAbstract uses dynamic import('@/lib/server/build-llm')
30
+ // In a test environment without the full server, the import will fail
31
+ // and the catch block returns fallbackAbstract
32
+ const longContent = 'B'.repeat(250)
33
+ const result = await generateAbstract(longContent, 'title')
34
+ // Fallback: first 150 chars + '...'
35
+ assert.ok(result !== null)
36
+ assert.equal(result, 'B'.repeat(150) + '...')
37
+ })
38
+
39
+ it('returns content as-is when <= 150 chars and LLM fails', async () => {
40
+ // This case won't trigger because content <= 200 returns null.
41
+ // The fallback only runs for content > 200 chars.
42
+ // For content > 200 but fallback returns first 150 + '...'
43
+ const content = 'C'.repeat(201)
44
+ const result = await generateAbstract(content, 'title')
45
+ assert.ok(result !== null)
46
+ assert.equal(result, 'C'.repeat(150) + '...')
47
+ })
48
+
49
+ it('fallback does not add ellipsis for content <= 150 chars', async () => {
50
+ // This is a unit test of the fallback logic itself:
51
+ // Since generateAbstract returns null for content <= 200,
52
+ // the fallback truncation (150 + '...') only applies to content > 200.
53
+ // But the fallbackAbstract function itself handles <= 150 without ellipsis.
54
+ // We test this indirectly — content of 201 chars gets truncated to 150 + '...'
55
+ const content = 'D'.repeat(201)
56
+ const result = await generateAbstract(content)
57
+ assert.equal(result, 'D'.repeat(150) + '...')
58
+ })
59
+ })