@swarmclawai/swarmclaw 1.2.4 → 1.2.6

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 (262) hide show
  1. package/README.md +14 -0
  2. package/bin/daemon-cmd.js +169 -0
  3. package/bin/server-cmd.js +3 -0
  4. package/bin/swarmclaw.js +11 -0
  5. package/package.json +17 -16
  6. package/src/app/api/agents/[id]/clone/route.ts +3 -32
  7. package/src/app/api/agents/[id]/route.ts +6 -158
  8. package/src/app/api/agents/[id]/status/route.ts +2 -3
  9. package/src/app/api/agents/[id]/thread/route.ts +4 -17
  10. package/src/app/api/agents/bulk/route.ts +5 -47
  11. package/src/app/api/agents/route.ts +5 -119
  12. package/src/app/api/agents/trash/route.ts +13 -24
  13. package/src/app/api/auth/route.ts +3 -9
  14. package/src/app/api/autonomy/estop/route.ts +5 -5
  15. package/src/app/api/chatrooms/[id]/chat/route.ts +11 -5
  16. package/src/app/api/chatrooms/[id]/route.ts +23 -2
  17. package/src/app/api/chatrooms/route.ts +13 -2
  18. package/src/app/api/chats/[id]/clear/route.ts +2 -13
  19. package/src/app/api/chats/[id]/deploy/route.ts +2 -3
  20. package/src/app/api/chats/[id]/edit-resend/route.ts +7 -13
  21. package/src/app/api/chats/[id]/mailbox/route.ts +6 -8
  22. package/src/app/api/chats/[id]/queue/route.ts +17 -64
  23. package/src/app/api/chats/[id]/retry/route.ts +4 -22
  24. package/src/app/api/chats/[id]/route.ts +10 -138
  25. package/src/app/api/chats/heartbeat/route.ts +2 -1
  26. package/src/app/api/chats/migrate-messages/route.ts +7 -0
  27. package/src/app/api/chats/route.ts +13 -134
  28. package/src/app/api/connectors/[id]/access/route.ts +12 -229
  29. package/src/app/api/connectors/[id]/doctor/route.ts +1 -1
  30. package/src/app/api/connectors/[id]/health/route.ts +12 -39
  31. package/src/app/api/connectors/[id]/route.ts +14 -122
  32. package/src/app/api/connectors/[id]/webhook/route.ts +1 -1
  33. package/src/app/api/connectors/doctor/route.ts +1 -1
  34. package/src/app/api/connectors/route.ts +12 -70
  35. package/src/app/api/credentials/[id]/route.ts +2 -4
  36. package/src/app/api/credentials/route.ts +10 -19
  37. package/src/app/api/daemon/health-check/route.ts +3 -4
  38. package/src/app/api/daemon/route.ts +10 -8
  39. package/src/app/api/documents/route.ts +11 -10
  40. package/src/app/api/external-agents/route.ts +3 -3
  41. package/src/app/api/gateways/[id]/health/route.ts +2 -3
  42. package/src/app/api/gateways/[id]/route.ts +7 -122
  43. package/src/app/api/gateways/route.ts +3 -103
  44. package/src/app/api/mcp-servers/[id]/tools/route.ts +5 -5
  45. package/src/app/api/openclaw/dashboard-url/route.ts +8 -16
  46. package/src/app/api/openclaw/directory/route.ts +2 -2
  47. package/src/app/api/openclaw/history/route.ts +3 -5
  48. package/src/app/api/providers/[id]/route.test.ts +49 -0
  49. package/src/app/api/providers/ollama/route.ts +6 -5
  50. package/src/app/api/schedules/[id]/route.ts +14 -108
  51. package/src/app/api/schedules/[id]/run/route.ts +6 -67
  52. package/src/app/api/schedules/route.ts +9 -51
  53. package/src/app/api/settings/route.ts +4 -3
  54. package/src/app/api/setup/check-provider/route.ts +23 -1
  55. package/src/app/api/setup/openclaw-device/route.ts +2 -2
  56. package/src/app/api/system/status/route.ts +2 -2
  57. package/src/app/api/tasks/[id]/route.ts +16 -202
  58. package/src/app/api/tasks/bulk/route.ts +5 -86
  59. package/src/app/api/tasks/metrics/route.ts +2 -1
  60. package/src/app/api/tasks/route.ts +11 -171
  61. package/src/app/api/upload/route.ts +1 -1
  62. package/src/app/api/uploads/[filename]/route.ts +1 -1
  63. package/src/app/api/uploads/route.ts +1 -1
  64. package/src/app/api/webhooks/[id]/history/route.ts +2 -2
  65. package/src/app/layout.tsx +9 -6
  66. package/src/app/protocols/page.tsx +71 -89
  67. package/src/app/tasks/page.tsx +32 -32
  68. package/src/cli/index.js +1 -0
  69. package/src/cli/spec.js +1 -0
  70. package/src/components/agents/agent-sheet.tsx +5 -5
  71. package/src/components/auth/setup-wizard/index.tsx +4 -4
  72. package/src/components/auth/setup-wizard/step-agents.tsx +1 -1
  73. package/src/components/auth/setup-wizard/step-connect.tsx +1 -1
  74. package/src/components/auth/setup-wizard/utils.ts +1 -1
  75. package/src/components/chatrooms/chatroom-sheet.tsx +16 -276
  76. package/src/components/connectors/connector-list.tsx +26 -40
  77. package/src/components/connectors/connector-sheet.tsx +95 -149
  78. package/src/components/gateways/gateway-sheet.tsx +61 -110
  79. package/src/components/layout/live-query-sync.tsx +121 -0
  80. package/src/components/protocols/structured-session-launcher.tsx +24 -45
  81. package/src/components/providers/app-query-provider.tsx +17 -0
  82. package/src/components/providers/provider-list.tsx +60 -61
  83. package/src/components/providers/provider-sheet.tsx +74 -56
  84. package/src/components/skills/skill-list.tsx +5 -18
  85. package/src/components/skills/skill-sheet.tsx +21 -20
  86. package/src/components/skills/skills-workspace.tsx +48 -87
  87. package/src/components/tasks/task-card.tsx +20 -13
  88. package/src/components/tasks/task-column.tsx +22 -7
  89. package/src/components/tasks/task-list.tsx +8 -11
  90. package/src/components/tasks/task-sheet.tsx +111 -103
  91. package/src/features/agents/queries.ts +20 -0
  92. package/src/features/chatrooms/queries.ts +20 -0
  93. package/src/features/chats/queries.ts +27 -0
  94. package/src/features/connectors/queries.ts +145 -0
  95. package/src/features/credentials/queries.ts +37 -0
  96. package/src/features/extensions/queries.ts +26 -0
  97. package/src/features/external-agents/queries.ts +36 -0
  98. package/src/features/gateways/queries.ts +274 -0
  99. package/src/features/missions/queries.ts +23 -0
  100. package/src/features/projects/queries.ts +20 -0
  101. package/src/features/protocols/queries.ts +149 -0
  102. package/src/features/providers/queries.ts +142 -0
  103. package/src/features/settings/queries.ts +20 -0
  104. package/src/features/skills/queries.ts +182 -0
  105. package/src/features/tasks/queries.ts +189 -0
  106. package/src/hooks/use-ws.ts +3 -2
  107. package/src/lib/app/api-client.ts +2 -2
  108. package/src/lib/providers/index.test.ts +108 -0
  109. package/src/lib/providers/index.ts +38 -15
  110. package/src/lib/query/client.ts +17 -0
  111. package/src/lib/server/agents/agent-runtime-config.ts +1 -1
  112. package/src/lib/server/agents/agent-service.ts +429 -0
  113. package/src/lib/server/agents/agent-thread-session.ts +6 -5
  114. package/src/lib/server/agents/autonomy-contract.ts +1 -4
  115. package/src/lib/server/agents/delegation-advisory.test.ts +206 -0
  116. package/src/lib/server/agents/delegation-advisory.ts +251 -0
  117. package/src/lib/server/agents/main-agent-loop.ts +98 -40
  118. package/src/lib/server/agents/subagent-runtime.ts +12 -0
  119. package/src/lib/server/autonomy/supervisor-reflection.test.ts +20 -1
  120. package/src/lib/server/autonomy/supervisor-reflection.ts +39 -19
  121. package/src/lib/server/build-llm.ts +7 -15
  122. package/src/lib/server/capability-router.test.ts +70 -1
  123. package/src/lib/server/capability-router.ts +24 -99
  124. package/src/lib/server/chat-execution/chat-execution-utils.ts +0 -15
  125. package/src/lib/server/chat-execution/chat-streaming-utils.ts +2 -4
  126. package/src/lib/server/chat-execution/chat-turn-finalization.ts +77 -12
  127. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +4 -4
  128. package/src/lib/server/chat-execution/chat-turn-preflight.ts +2 -2
  129. package/src/lib/server/chat-execution/chat-turn-preparation.ts +41 -17
  130. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +4 -2
  131. package/src/lib/server/chat-execution/chat-turn-tool-routing.test.ts +45 -0
  132. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +48 -17
  133. package/src/lib/server/chat-execution/continuation-evaluator.ts +4 -1
  134. package/src/lib/server/chat-execution/direct-memory-intent.test.ts +9 -0
  135. package/src/lib/server/chat-execution/direct-memory-intent.ts +12 -2
  136. package/src/lib/server/chat-execution/message-classifier.test.ts +35 -23
  137. package/src/lib/server/chat-execution/message-classifier.ts +74 -32
  138. package/src/lib/server/chat-execution/prompt-builder.test.ts +29 -0
  139. package/src/lib/server/chat-execution/prompt-builder.ts +37 -2
  140. package/src/lib/server/chat-execution/prompt-sections.test.ts +56 -0
  141. package/src/lib/server/chat-execution/prompt-sections.ts +193 -0
  142. package/src/lib/server/chat-execution/stream-agent-chat.ts +63 -7
  143. package/src/lib/server/chat-execution/stream-continuation.test.ts +36 -0
  144. package/src/lib/server/chat-execution/stream-continuation.ts +28 -13
  145. package/src/lib/server/chatrooms/chatroom-agent-signals.ts +26 -18
  146. package/src/lib/server/chatrooms/chatroom-helpers.ts +19 -18
  147. package/src/lib/server/chatrooms/chatroom-repository.ts +16 -0
  148. package/src/lib/server/chatrooms/chatroom-routing.test.ts +96 -0
  149. package/src/lib/server/chatrooms/chatroom-routing.ts +207 -53
  150. package/src/lib/server/chatrooms/mailbox-utils.ts +4 -2
  151. package/src/lib/server/chatrooms/session-mailbox.ts +50 -40
  152. package/src/lib/server/chats/chat-session-service.ts +410 -0
  153. package/src/lib/server/connectors/access.ts +1 -1
  154. package/src/lib/server/connectors/commands.ts +7 -6
  155. package/src/lib/server/connectors/connector-inbound.ts +14 -7
  156. package/src/lib/server/connectors/connector-outbound.ts +16 -11
  157. package/src/lib/server/connectors/connector-service.ts +453 -0
  158. package/src/lib/server/connectors/delivery.ts +17 -12
  159. package/src/lib/server/connectors/inbound-audio-transcription.ts +5 -14
  160. package/src/lib/server/connectors/media.ts +1 -1
  161. package/src/lib/server/connectors/response-media.ts +1 -1
  162. package/src/lib/server/connectors/session-consolidation.ts +11 -7
  163. package/src/lib/server/connectors/session.ts +9 -7
  164. package/src/lib/server/connectors/voice-note.ts +2 -1
  165. package/src/lib/server/context-manager.ts +20 -1
  166. package/src/lib/server/cost.ts +2 -3
  167. package/src/lib/server/credentials/credential-repository.ts +43 -4
  168. package/src/lib/server/credentials/credential-service.ts +112 -0
  169. package/src/lib/server/daemon/admin-metadata.ts +64 -0
  170. package/src/lib/server/daemon/controller.ts +577 -0
  171. package/src/lib/server/daemon/daemon-runtime.ts +352 -0
  172. package/src/lib/server/daemon/daemon-status-repository.ts +63 -0
  173. package/src/lib/server/daemon/types.ts +101 -0
  174. package/src/lib/server/embeddings.ts +3 -9
  175. package/src/lib/server/eval/agent-regression.ts +3 -2
  176. package/src/lib/server/eval/runner.ts +2 -2
  177. package/src/lib/server/execution-brief.test.ts +167 -0
  178. package/src/lib/server/execution-brief.ts +295 -0
  179. package/src/lib/server/execution-engine/chat-turn.ts +9 -0
  180. package/src/lib/server/execution-engine/import-boundary.test.ts +44 -0
  181. package/src/lib/server/execution-engine/index.ts +35 -0
  182. package/src/lib/server/execution-engine/task-attempt.ts +303 -0
  183. package/src/lib/server/execution-engine/types.ts +33 -0
  184. package/src/lib/server/gateways/gateway-profile-repository.ts +47 -3
  185. package/src/lib/server/gateways/gateway-profile-service.ts +200 -0
  186. package/src/lib/server/memory/session-archive-memory.ts +12 -10
  187. package/src/lib/server/messages/message-repository.ts +330 -0
  188. package/src/lib/server/missions/mission-service/core.ts +8 -6
  189. package/src/lib/server/openclaw/agent-resolver.ts +2 -3
  190. package/src/lib/server/openclaw/doctor.ts +1 -1
  191. package/src/lib/server/openclaw/gateway.test.ts +10 -1
  192. package/src/lib/server/openclaw/gateway.ts +5 -14
  193. package/src/lib/server/openclaw/health.ts +3 -11
  194. package/src/lib/server/openclaw/sync.ts +8 -6
  195. package/src/lib/server/persistence/storage-context.ts +3 -0
  196. package/src/lib/server/protocols/protocol-agent-turn.ts +25 -17
  197. package/src/lib/server/protocols/protocol-normalization.ts +1 -1
  198. package/src/lib/server/protocols/protocol-queries.ts +13 -7
  199. package/src/lib/server/protocols/protocol-run-lifecycle.ts +16 -20
  200. package/src/lib/server/protocols/protocol-run-repository.ts +81 -0
  201. package/src/lib/server/protocols/protocol-step-processors.ts +23 -31
  202. package/src/lib/server/protocols/protocol-swarm.ts +8 -8
  203. package/src/lib/server/protocols/protocol-template-repository.ts +42 -0
  204. package/src/lib/server/protocols/protocol-templates.ts +4 -2
  205. package/src/lib/server/protocols/protocol-types.ts +10 -7
  206. package/src/lib/server/provider-endpoint.ts +7 -12
  207. package/src/lib/server/provider-model-discovery.ts +2 -11
  208. package/src/lib/server/query-expansion.ts +5 -6
  209. package/src/lib/server/run-context.test.ts +365 -0
  210. package/src/lib/server/run-context.ts +367 -0
  211. package/src/lib/server/runtime/heartbeat-service.ts +7 -5
  212. package/src/lib/server/runtime/queue/core.ts +61 -190
  213. package/src/lib/server/runtime/run-ledger.ts +8 -0
  214. package/src/lib/server/runtime/session-run-manager/drain.ts +2 -2
  215. package/src/lib/server/runtime/session-run-manager/enqueue.ts +6 -0
  216. package/src/lib/server/runtime/session-run-manager/state.ts +4 -0
  217. package/src/lib/server/schedules/schedule-route-service.ts +230 -0
  218. package/src/lib/server/service-result.ts +16 -0
  219. package/src/lib/server/session-note.ts +2 -3
  220. package/src/lib/server/session-reset-policy.ts +4 -3
  221. package/src/lib/server/session-tools/connector.ts +9 -6
  222. package/src/lib/server/session-tools/context-mgmt.ts +58 -9
  223. package/src/lib/server/session-tools/crud.ts +162 -10
  224. package/src/lib/server/session-tools/delegate.ts +1 -1
  225. package/src/lib/server/session-tools/manage-tasks.test.ts +152 -0
  226. package/src/lib/server/session-tools/memory.ts +6 -4
  227. package/src/lib/server/session-tools/session-info.test.ts +56 -0
  228. package/src/lib/server/session-tools/session-info.ts +119 -12
  229. package/src/lib/server/session-tools/skill-runtime.ts +3 -1
  230. package/src/lib/server/session-tools/skills.ts +15 -15
  231. package/src/lib/server/session-tools/subagent.test.ts +115 -1
  232. package/src/lib/server/session-tools/subagent.ts +125 -7
  233. package/src/lib/server/session-tools/team-context.ts +4 -3
  234. package/src/lib/server/session-tools/wallet.ts +0 -58
  235. package/src/lib/server/sessions/session-lineage.ts +55 -0
  236. package/src/lib/server/sessions/session-repository.ts +2 -2
  237. package/src/lib/server/skills/learned-skills.ts +24 -23
  238. package/src/lib/server/skills/runtime-skill-resolver.ts +2 -1
  239. package/src/lib/server/skills/skill-repository.ts +136 -13
  240. package/src/lib/server/skills/skill-suggestions.ts +25 -28
  241. package/src/lib/server/storage-normalization.test.ts +44 -267
  242. package/src/lib/server/storage-normalization.ts +75 -0
  243. package/src/lib/server/storage.ts +19 -0
  244. package/src/lib/server/structured-extract.ts +3 -14
  245. package/src/lib/server/tasks/task-followups.ts +16 -11
  246. package/src/lib/server/tasks/task-result.test.ts +25 -29
  247. package/src/lib/server/tasks/task-result.ts +5 -9
  248. package/src/lib/server/tasks/task-route-service.ts +449 -0
  249. package/src/lib/server/text-normalization.ts +41 -0
  250. package/src/lib/server/tool-planning.ts +6 -42
  251. package/src/lib/server/upload-path.ts +5 -0
  252. package/src/lib/server/working-state/extraction.ts +614 -0
  253. package/src/lib/server/working-state/normalization.ts +866 -0
  254. package/src/lib/server/working-state/prompt.ts +60 -0
  255. package/src/lib/server/working-state/repository.ts +38 -0
  256. package/src/lib/server/working-state/service.test.ts +253 -0
  257. package/src/lib/server/working-state/service.ts +293 -0
  258. package/src/lib/validation/schemas.ts +1 -0
  259. package/src/lib/ws-client.ts +3 -3
  260. package/src/stores/slices/task-slice.ts +1 -4
  261. package/src/stores/use-chatroom-store.ts +2 -2
  262. package/src/types/index.ts +277 -12
@@ -1,14 +1,18 @@
1
1
  import fs from 'fs'
2
2
  import os from 'os'
3
3
  import path from 'path'
4
- import { loadSettings, loadSkills, loadCredentials, decryptKey, loadSessions, saveSessions } from '@/lib/server/storage'
5
4
  import { buildCurrentDateTimePromptContext } from '@/lib/server/prompt-runtime-context'
6
5
  import { buildIdentityContinuityContext } from '@/lib/server/identity-continuity'
7
6
  import { genId } from '@/lib/id'
8
7
  import { WORKSPACE_DIR } from '@/lib/server/data-dir'
9
8
  import { applyResolvedRoute, resolvePrimaryAgentRoute } from '@/lib/server/agents/agent-runtime-config'
9
+ import { loadCredential, decryptKey } from '@/lib/server/credentials/credential-repository'
10
10
  import { resolveProviderApiEndpoint, resolveProviderCredentialId } from '@/lib/server/provider-endpoint'
11
+ import { loadSettings } from '@/lib/server/settings/settings-repository'
11
12
  import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
13
+ import { loadSkills } from '@/lib/server/skills/skill-repository'
14
+ import { loadSession, patchSession, saveSession } from '@/lib/server/sessions/session-repository'
15
+ import { appendMessage } from '@/lib/server/messages/message-repository'
12
16
  import type { Chatroom, ChatroomMember, Agent, Session, Message, ChatroomMessage } from '@/types'
13
17
  import { getEnabledCapabilityIds, getEnabledToolIds } from '@/lib/capability-selection'
14
18
 
@@ -16,8 +20,7 @@ import { getEnabledCapabilityIds, getEnabledToolIds } from '@/lib/capability-sel
16
20
  export function resolveApiKey(credentialId: string | null | undefined): string | null {
17
21
  const resolvedCredentialId = resolveProviderCredentialId({ credentialId })
18
22
  if (!resolvedCredentialId) return null
19
- const creds = loadCredentials()
20
- const cred = creds[resolvedCredentialId]
23
+ const cred = loadCredential(resolvedCredentialId)
21
24
  if (!cred?.encryptedKey) return null
22
25
  try { return decryptKey(cred.encryptedKey) } catch { return null }
23
26
  }
@@ -136,6 +139,7 @@ export function parseMentions(
136
139
 
137
140
  // Check if the only explicit matches are the sender — if so, treat as "no explicit mentions"
138
141
  const senderId = opts?.senderId
142
+ const explicitSelfMentioned = senderId ? mentioned.includes(senderId) : false
139
143
  const explicitNonSelf = senderId ? mentioned.filter((id) => id !== senderId) : mentioned
140
144
 
141
145
  // 2. Reply-based implicit mention
@@ -158,8 +162,9 @@ export function parseMentions(
158
162
  }
159
163
  }
160
164
 
161
- // Remove self from final list when senderId is provided
162
- return senderId ? mentioned.filter((mid) => mid !== senderId) : mentioned
165
+ // Preserve explicit self-mentions so agents can intentionally address themselves.
166
+ if (!senderId || explicitSelfMentioned) return mentioned
167
+ return mentioned.filter((mid) => mid !== senderId)
163
168
  }
164
169
 
165
170
  export function resolveReplyTargetAgentId(
@@ -312,9 +317,8 @@ export function ensureSyntheticSession(agent: Agent, chatroomId: string): Sessio
312
317
  const roomWorkspace = resolveChatroomWorkspaceDir(chatroomId)
313
318
  fs.mkdirSync(roomWorkspace, { recursive: true })
314
319
  const sessionId = resolveSyntheticSessionId(chatroomId, agent.id)
315
- const sessions = loadSessions()
316
320
  const now = Date.now()
317
- const existing = sessions[sessionId]
321
+ const existing = loadSession(sessionId)
318
322
  const session: Session = existing
319
323
  ? applyResolvedRoute({
320
324
  ...existing,
@@ -348,8 +352,7 @@ export function ensureSyntheticSession(agent: Agent, chatroomId: string): Sessio
348
352
  }
349
353
  if (session.codexThreadId === undefined) session.codexThreadId = null
350
354
  if (session.opencodeSessionId === undefined) session.opencodeSessionId = null
351
- sessions[sessionId] = session
352
- saveSessions(sessions)
355
+ saveSession(sessionId, session)
353
356
  return session
354
357
  }
355
358
 
@@ -360,18 +363,16 @@ export function appendSyntheticSessionMessage(
360
363
  ): void {
361
364
  const trimmed = String(text || '').trim()
362
365
  if (!trimmed) return
363
- const sessions = loadSessions()
364
- const session = sessions[sessionId]
365
- if (!session) return
366
- if (!Array.isArray(session.messages)) session.messages = []
367
- session.messages.push({
366
+ const timestamp = Date.now()
367
+ appendMessage(sessionId, {
368
368
  role,
369
369
  text: trimmed,
370
- time: Date.now(),
370
+ time: timestamp,
371
+ })
372
+ patchSession(sessionId, (current) => {
373
+ if (!current) return null
374
+ return { ...current, lastActiveAt: timestamp }
371
375
  })
372
- session.lastActiveAt = Date.now()
373
- sessions[sessionId] = session
374
- saveSessions(sessions)
375
376
  }
376
377
 
377
378
  /** Build agent's system prompt including skills and identity context */
@@ -1,10 +1,13 @@
1
1
  import type { Chatroom } from '@/types'
2
2
 
3
3
  import {
4
+ deleteStoredItem,
4
5
  loadChatroom as loadStoredChatroom,
5
6
  loadChatrooms as loadStoredChatrooms,
7
+ patchStoredItem,
6
8
  saveChatrooms as saveStoredChatrooms,
7
9
  upsertChatroom as upsertStoredChatroom,
10
+ upsertStoredItems,
8
11
  } from '@/lib/server/storage'
9
12
  import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
10
13
 
@@ -20,13 +23,26 @@ export const chatroomRepository = createRecordRepository<Chatroom>(
20
23
  upsert(id, value) {
21
24
  upsertStoredChatroom(id, value as Chatroom)
22
25
  },
26
+ upsertMany(entries) {
27
+ upsertStoredItems('chatrooms', entries as Array<[string, Chatroom]>)
28
+ },
29
+ patch(id, updater) {
30
+ return patchStoredItem('chatrooms', id, updater as (current: Chatroom | null) => Chatroom | null) as Chatroom | null
31
+ },
23
32
  replace(data) {
24
33
  saveStoredChatrooms(data)
25
34
  },
35
+ delete(id) {
36
+ deleteStoredItem('chatrooms', id)
37
+ },
26
38
  },
27
39
  )
28
40
 
29
41
  export const loadChatrooms = () => chatroomRepository.list()
30
42
  export const loadChatroom = (id: string) => chatroomRepository.get(id)
43
+ export const loadChatroomMany = (ids: string[]) => chatroomRepository.getMany(ids)
31
44
  export const saveChatrooms = (items: Record<string, Chatroom | Record<string, unknown>>) => chatroomRepository.replace(items as Record<string, Chatroom>)
32
45
  export const upsertChatroom = (id: string, value: Chatroom | Record<string, unknown>) => chatroomRepository.upsert(id, value as Chatroom)
46
+ export const upsertChatrooms = (entries: Array<[string, Chatroom | Record<string, unknown>]>) => chatroomRepository.upsertMany(entries as Array<[string, Chatroom]>)
47
+ export const patchChatroom = (id: string, updater: (current: Chatroom | null) => Chatroom | null) => chatroomRepository.patch(id, updater)
48
+ export const deleteChatroom = (id: string) => chatroomRepository.delete(id)
@@ -0,0 +1,96 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import type { Agent, Chatroom } from '@/types'
4
+ import {
5
+ ensureChatroomRoutingGuidance,
6
+ selectChatroomRecipients,
7
+ synthesizeRoutingGuidanceFromRules,
8
+ } from './chatroom-routing'
9
+
10
+ const agents: Record<string, Agent> = {
11
+ ops: {
12
+ id: 'ops',
13
+ name: 'Ops',
14
+ description: 'Handles deploys and infrastructure',
15
+ provider: 'openai',
16
+ model: 'gpt-test',
17
+ systemPrompt: '',
18
+ capabilities: ['deploy', 'infrastructure'],
19
+ },
20
+ design: {
21
+ id: 'design',
22
+ name: 'Design',
23
+ description: 'Handles design critique and UI polish',
24
+ provider: 'openai',
25
+ model: 'gpt-test',
26
+ systemPrompt: '',
27
+ capabilities: ['design', 'ui'],
28
+ },
29
+ }
30
+
31
+ function makeChatroom(overrides: Partial<Chatroom> = {}): Chatroom {
32
+ return {
33
+ id: 'room-1',
34
+ name: 'Test Room',
35
+ description: 'General routing test room',
36
+ agentIds: ['ops', 'design'],
37
+ messages: [],
38
+ createdAt: 1,
39
+ updatedAt: 1,
40
+ ...overrides,
41
+ }
42
+ }
43
+
44
+ test('synthesizes guidance from legacy routing rules and migrates the chatroom', () => {
45
+ const chatroom = makeChatroom({
46
+ routingRules: [
47
+ { id: 'rule-1', type: 'keyword', keywords: ['deploy', 'release'], agentId: 'ops', priority: 1 },
48
+ { id: 'rule-2', type: 'capability', pattern: 'design review', agentId: 'design', priority: 2 },
49
+ ],
50
+ })
51
+
52
+ const guidance = synthesizeRoutingGuidanceFromRules(chatroom.routingRules, agents)
53
+ assert.match(String(guidance || ''), /deploy/i)
54
+ assert.match(String(guidance || ''), /Design/i)
55
+
56
+ const changed = ensureChatroomRoutingGuidance(chatroom, agents)
57
+ assert.equal(changed, true)
58
+ assert.equal(typeof chatroom.routingGuidance, 'string')
59
+ assert.equal(chatroom.routingRules, undefined)
60
+ })
61
+
62
+ test('selects only member ids returned by the selector model', async () => {
63
+ const chatroom = makeChatroom({
64
+ routingGuidance: 'Route deployment incidents to Ops. Prefer Design for UI critique.',
65
+ })
66
+
67
+ const selected = await selectChatroomRecipients({
68
+ text: 'Please diagnose the failed deployment.',
69
+ chatroom,
70
+ agentsById: agents,
71
+ }, {
72
+ generateText: async () => '{"agentIds":["ops","non-member","ops"]}',
73
+ })
74
+
75
+ assert.deepEqual(selected, ['ops'])
76
+ })
77
+
78
+ test('fails open to no inferred mentions when there is no guidance or the selector output is invalid', async () => {
79
+ const unguided = await selectChatroomRecipients({
80
+ text: 'Anyone here?',
81
+ chatroom: makeChatroom(),
82
+ agentsById: agents,
83
+ }, {
84
+ generateText: async () => '{"agentIds":["ops"]}',
85
+ })
86
+ assert.deepEqual(unguided, [])
87
+
88
+ const invalid = await selectChatroomRecipients({
89
+ text: 'Please review the new layout.',
90
+ chatroom: makeChatroom({ routingGuidance: 'Prefer Design for UI review.' }),
91
+ agentsById: agents,
92
+ }, {
93
+ generateText: async () => 'not-json',
94
+ })
95
+ assert.deepEqual(invalid, [])
96
+ })
@@ -1,66 +1,220 @@
1
- import type { ChatroomRoutingRule, Agent } from '@/types'
2
- import { matchesCapabilities } from '@/lib/server/agents/capability-match'
1
+ import { HumanMessage } from '@langchain/core/messages'
2
+ import type { Agent, Chatroom, ChatroomRoutingRule } from '@/types'
3
+ import { buildLLM } from '@/lib/server/build-llm'
4
+ import { log } from '@/lib/server/logger'
3
5
 
4
- /**
5
- * Evaluate routing rules against inbound message text.
6
- *
7
- * Rules are evaluated in priority order (lower number = higher priority).
8
- * First match wins — returns the matched agentIds.
9
- *
10
- * - 'keyword' rules: case-insensitive substring match against `keywords[]`,
11
- * or regex match against `pattern`.
12
- * - 'capability' rules: match `pattern` against each agent's `capabilities[]`.
13
- */
14
- export function evaluateRoutingRules(
15
- text: string,
16
- rules: ChatroomRoutingRule[],
17
- agents: Agent[],
18
- ): string[] {
19
- if (!rules.length) return []
6
+ const TAG = 'chatroom-routing'
7
+ const SELECTOR_TIMEOUT_MS = 4_000
20
8
 
21
- const sorted = [...rules].sort((a, b) => a.priority - b.priority)
22
- const lowerText = text.toLowerCase()
9
+ interface ChatroomRecipientSelection {
10
+ agentIds: string[]
11
+ }
23
12
 
24
- for (const rule of sorted) {
25
- if (rule.type === 'keyword') {
26
- let matched = false
13
+ function normalizeGuidance(value: string | null | undefined): string | null {
14
+ const trimmed = typeof value === 'string' ? value.trim() : ''
15
+ return trimmed || null
16
+ }
27
17
 
28
- // Check keywords (case-insensitive substring)
29
- if (rule.keywords?.length) {
30
- matched = rule.keywords.some((kw) => lowerText.includes(kw.toLowerCase()))
18
+ function extractFirstJsonObject(text: string): string | null {
19
+ const source = String(text || '').trim()
20
+ if (!source) return null
21
+ let start = -1
22
+ let depth = 0
23
+ let inString = false
24
+ let escaped = false
25
+ for (let index = 0; index < source.length; index += 1) {
26
+ const char = source[index]
27
+ if (start === -1) {
28
+ if (char === '{') {
29
+ start = index
30
+ depth = 1
31
31
  }
32
+ continue
33
+ }
34
+ if (inString) {
35
+ if (escaped) escaped = false
36
+ else if (char === '\\') escaped = true
37
+ else if (char === '"') inString = false
38
+ continue
39
+ }
40
+ if (char === '"') {
41
+ inString = true
42
+ continue
43
+ }
44
+ if (char === '{') depth += 1
45
+ else if (char === '}') depth -= 1
46
+ if (depth === 0) return source.slice(start, index + 1)
47
+ }
48
+ return null
49
+ }
32
50
 
33
- // Check pattern (regex)
34
- if (!matched && rule.pattern) {
35
- try {
36
- const re = new RegExp(rule.pattern, 'i')
37
- matched = re.test(text)
38
- } catch {
39
- // Invalid regex — skip
40
- }
41
- }
51
+ function extractModelText(content: unknown): string {
52
+ if (typeof content === 'string') return content
53
+ if (!Array.isArray(content)) return ''
54
+ return content
55
+ .map((part) => (part && typeof part === 'object' && 'text' in part && typeof part.text === 'string') ? part.text : '')
56
+ .join('')
57
+ }
42
58
 
43
- if (matched) return [rule.agentId]
59
+ function parseRecipientSelection(text: string, allowedAgentIds: Set<string>): string[] {
60
+ const jsonText = extractFirstJsonObject(text)
61
+ if (!jsonText) return []
62
+ try {
63
+ const parsed = JSON.parse(jsonText) as Partial<ChatroomRecipientSelection>
64
+ if (!Array.isArray(parsed.agentIds)) return []
65
+ const seen = new Set<string>()
66
+ const selected: string[] = []
67
+ for (const value of parsed.agentIds) {
68
+ if (typeof value !== 'string') continue
69
+ const agentId = value.trim()
70
+ if (!agentId || !allowedAgentIds.has(agentId) || seen.has(agentId)) continue
71
+ seen.add(agentId)
72
+ selected.push(agentId)
44
73
  }
74
+ return selected
75
+ } catch {
76
+ return []
77
+ }
78
+ }
79
+
80
+ function formatLegacyRule(rule: ChatroomRoutingRule, agentsById: Record<string, Agent | undefined>): string | null {
81
+ const agentName = agentsById[rule.agentId]?.name || rule.agentId
82
+ if (rule.type === 'keyword') {
83
+ const parts = [
84
+ Array.isArray(rule.keywords) && rule.keywords.length > 0
85
+ ? `topics or phrases like ${rule.keywords.map((keyword) => `"${keyword}"`).join(', ')}`
86
+ : null,
87
+ rule.pattern ? `messages matching ${JSON.stringify(rule.pattern)}` : null,
88
+ ].filter(Boolean)
89
+ if (parts.length === 0) return null
90
+ return `Priority ${rule.priority}: route ${parts.join(' or ')} to ${agentName}.`
91
+ }
92
+ if (!rule.pattern) return null
93
+ return `Priority ${rule.priority}: prefer ${agentName} when the request best fits capability area ${JSON.stringify(rule.pattern)}.`
94
+ }
95
+
96
+ export function synthesizeRoutingGuidanceFromRules(
97
+ rules: ChatroomRoutingRule[] | null | undefined,
98
+ agentsById: Record<string, Agent | undefined>,
99
+ ): string | null {
100
+ if (!Array.isArray(rules) || rules.length === 0) return null
101
+ const lines = rules
102
+ .slice()
103
+ .sort((a, b) => a.priority - b.priority)
104
+ .map((rule) => formatLegacyRule(rule, agentsById))
105
+ .filter((line): line is string => typeof line === 'string' && line.trim().length > 0)
106
+ if (lines.length === 0) return null
107
+ return [
108
+ 'Legacy routing guidance synthesized from older routing rules. Earlier priorities take precedence when multiple agents could fit.',
109
+ ...lines,
110
+ ].join('\n')
111
+ }
45
112
 
46
- if (rule.type === 'capability') {
47
- if (!rule.pattern) continue
48
- const patternLower = rule.pattern.toLowerCase()
113
+ export function resolveChatroomRoutingGuidance(
114
+ chatroom: Chatroom,
115
+ agentsById: Record<string, Agent | undefined>,
116
+ ): string | null {
117
+ return normalizeGuidance(chatroom.routingGuidance)
118
+ || synthesizeRoutingGuidanceFromRules(chatroom.routingRules, agentsById)
119
+ }
49
120
 
50
- // Check if the specific agent has a matching capability
51
- const agent = agents.find((a) => a.id === rule.agentId)
52
- if (agent && matchesCapabilities(agent.capabilities, [rule.pattern])) {
53
- // Only match if the message text is relevant to the capability
54
- // Use the pattern as a keyword match against the message text too
55
- try {
56
- const re = new RegExp(rule.pattern, 'i')
57
- if (re.test(text)) return [rule.agentId]
58
- } catch {
59
- if (lowerText.includes(patternLower)) return [rule.agentId]
60
- }
121
+ export function ensureChatroomRoutingGuidance(
122
+ chatroom: Chatroom,
123
+ agentsById: Record<string, Agent | undefined>,
124
+ ): boolean {
125
+ const guidance = resolveChatroomRoutingGuidance(chatroom, agentsById)
126
+ const nextGuidance = normalizeGuidance(guidance)
127
+ const hadRules = Array.isArray(chatroom.routingRules) && chatroom.routingRules.length > 0
128
+ const guidanceChanged = nextGuidance !== normalizeGuidance(chatroom.routingGuidance)
129
+ if (!guidanceChanged && !hadRules) return false
130
+ chatroom.routingGuidance = nextGuidance
131
+ delete chatroom.routingRules
132
+ return guidanceChanged || hadRules
133
+ }
134
+
135
+ function buildRecipientSelectionPrompt(params: {
136
+ text: string
137
+ chatroom: Chatroom
138
+ guidance: string
139
+ members: Array<{
140
+ id: string
141
+ name: string
142
+ description: string
143
+ capabilities: string[]
144
+ }>
145
+ }): string {
146
+ return [
147
+ 'Choose which chatroom members should receive the latest message.',
148
+ 'Return JSON only.',
149
+ 'Use only agent IDs from the provided member list.',
150
+ 'Prefer the smallest relevant set. Return an empty array when no routing guidance clearly applies.',
151
+ 'Respect explicit routing guidance over generic capability overlap.',
152
+ '',
153
+ 'Output shape:',
154
+ '{"agentIds":["agent-id-1","agent-id-2"]}',
155
+ '',
156
+ `Chatroom description: ${JSON.stringify(params.chatroom.description || '')}`,
157
+ `Routing guidance: ${JSON.stringify(params.guidance)}`,
158
+ `Latest message: ${JSON.stringify(params.text)}`,
159
+ 'Members:',
160
+ JSON.stringify(params.members),
161
+ ].join('\n')
162
+ }
163
+
164
+ export async function selectChatroomRecipients(
165
+ params: {
166
+ text: string
167
+ chatroom: Chatroom
168
+ agentsById: Record<string, Agent | undefined>
169
+ },
170
+ hooks?: {
171
+ generateText?: (prompt: string) => Promise<string>
172
+ },
173
+ ): Promise<string[]> {
174
+ const guidance = resolveChatroomRoutingGuidance(params.chatroom, params.agentsById)
175
+ if (!guidance) return []
176
+
177
+ const members = params.chatroom.agentIds
178
+ .map((agentId) => {
179
+ const agent = params.agentsById[agentId]
180
+ if (!agent) return null
181
+ return {
182
+ id: agent.id,
183
+ name: agent.name,
184
+ description: agent.description || '',
185
+ capabilities: Array.isArray(agent.capabilities) ? agent.capabilities.slice(0, 12) : [],
61
186
  }
62
- }
63
- }
187
+ })
188
+ .filter((member): member is NonNullable<typeof member> => member !== null)
189
+ if (members.length === 0) return []
190
+
191
+ const prompt = buildRecipientSelectionPrompt({
192
+ text: params.text,
193
+ chatroom: params.chatroom,
194
+ guidance,
195
+ members,
196
+ })
197
+ const allowedAgentIds = new Set(members.map((member) => member.id))
64
198
 
65
- return []
199
+ try {
200
+ const responseText = await Promise.race([
201
+ hooks?.generateText
202
+ ? hooks.generateText(prompt)
203
+ : (async () => {
204
+ const { llm } = await buildLLM()
205
+ const response = await llm.invoke([new HumanMessage(prompt)])
206
+ return extractModelText(response.content)
207
+ })(),
208
+ new Promise<never>((_, reject) => {
209
+ setTimeout(() => reject(new Error('chatroom-recipient-selector-timeout')), SELECTOR_TIMEOUT_MS)
210
+ }),
211
+ ])
212
+ return parseRecipientSelection(responseText, allowedAgentIds)
213
+ } catch (error: unknown) {
214
+ log.warn(TAG, 'Failed to select chatroom recipients from routing guidance', {
215
+ error: error instanceof Error ? error.message : 'unknown',
216
+ chatroomId: params.chatroom.id,
217
+ })
218
+ return []
219
+ }
66
220
  }
@@ -3,8 +3,10 @@ import path from 'path'
3
3
  import { ImapFlow } from 'imapflow'
4
4
  import { createTransport } from 'nodemailer'
5
5
  import { simpleParser } from 'mailparser'
6
- import { UPLOAD_DIR, loadConnectors } from '@/lib/server/storage'
6
+ import type { Connector } from '@/types'
7
+ import { loadConnectors } from '@/lib/server/connectors/connector-repository'
7
8
  import { getExtensionManager } from '@/lib/server/extensions'
9
+ import { UPLOAD_DIR } from '@/lib/server/upload-path'
8
10
 
9
11
  export interface MailboxConfig {
10
12
  imapHost: string
@@ -79,7 +81,7 @@ export function getMailboxConfig(): MailboxConfig {
79
81
  const emailSettings = extensionManager.getExtensionSettings('email') as Record<string, unknown>
80
82
  const connectors = loadConnectors()
81
83
  const emailConnector = Object.values(connectors)
82
- .find((entry) => entry && typeof entry === 'object' && String((entry as Record<string, unknown>).platform || '').toLowerCase() === 'email') as Record<string, unknown> | undefined
84
+ .find((entry) => entry.platform === 'email') as Connector | undefined
83
85
  const connectorConfig = emailConnector && typeof emailConnector.config === 'object' && emailConnector.config
84
86
  ? emailConnector.config as Record<string, unknown>
85
87
  : {}
@@ -1,6 +1,6 @@
1
1
  import { genId } from '@/lib/id'
2
2
  import type { MailboxEnvelope } from '@/types'
3
- import { loadSessions, saveSessions } from '@/lib/server/storage'
3
+ import { loadSession, patchSession } from '@/lib/server/sessions/session-repository'
4
4
  import { requestMissionTicksForHumanReply } from '@/lib/server/missions/mission-service'
5
5
 
6
6
  interface MailboxOptions {
@@ -83,9 +83,8 @@ function normalizeMailbox(target: { mailbox?: MailboxEnvelope[] | null }, now =
83
83
 
84
84
  function findLatestPendingHumanRequestEnvelope(
85
85
  sessionId: string,
86
- sessions = loadSessions(),
86
+ target = loadSession(sessionId),
87
87
  ): MailboxEnvelope | null {
88
- const target = sessions[sessionId]
89
88
  if (!target) throw new Error(`Session not found: ${sessionId}`)
90
89
  const envelopes = normalizeMailbox(target)
91
90
  const repliedCorrelationIds = new Set(
@@ -108,8 +107,7 @@ export function findPendingHumanRequestEnvelope(params: {
108
107
  fromSessionId?: string | null
109
108
  fromAgentId?: string | null
110
109
  }): MailboxEnvelope | null {
111
- const sessions = loadSessions()
112
- const target = sessions[params.sessionId]
110
+ const target = loadSession(params.sessionId)
113
111
  if (!target) throw new Error(`Session not found: ${params.sessionId}`)
114
112
  const expectedSignature = normalizeHumanRequestSignature(params)
115
113
  const envelopes = normalizeMailbox(target)
@@ -139,8 +137,7 @@ export function sendMailboxEnvelope(input: {
139
137
  correlationId?: string | null
140
138
  ttlSec?: number | null
141
139
  }): MailboxEnvelope {
142
- const sessions = loadSessions()
143
- const target = sessions[input.toSessionId]
140
+ const target = loadSession(input.toSessionId)
144
141
  if (!target) throw new Error(`Target session not found: ${input.toSessionId}`)
145
142
 
146
143
  const now = Date.now()
@@ -162,12 +159,14 @@ export function sendMailboxEnvelope(input: {
162
159
  ackAt: null,
163
160
  }
164
161
 
165
- const existing = normalizeMailbox(target, now)
166
- existing.push(envelope)
167
- target.mailbox = existing
168
- target.lastActiveAt = now
169
- sessions[input.toSessionId] = target
170
- saveSessions(sessions)
162
+ patchSession(input.toSessionId, (current) => {
163
+ if (!current) return null
164
+ return {
165
+ ...current,
166
+ mailbox: [...normalizeMailbox(current, now), envelope],
167
+ lastActiveAt: now,
168
+ }
169
+ })
171
170
  if (envelope.type === 'human_reply') {
172
171
  requestMissionTicksForHumanReply({
173
172
  sessionId: input.toSessionId,
@@ -188,8 +187,7 @@ export function sendMailboxEnvelope(input: {
188
187
  }
189
188
 
190
189
  export function listMailbox(sessionId: string, opts: MailboxOptions = {}): MailboxEnvelope[] {
191
- const sessions = loadSessions()
192
- const target = sessions[sessionId]
190
+ const target = loadSession(sessionId)
193
191
  if (!target) throw new Error(`Session not found: ${sessionId}`)
194
192
  const list = pruneExpired(normalizeMailboxList(target.mailbox || []))
195
193
  const includeAcked = opts.includeAcked === true
@@ -201,36 +199,48 @@ export function listMailbox(sessionId: string, opts: MailboxOptions = {}): Mailb
201
199
  }
202
200
 
203
201
  export function ackMailboxEnvelope(sessionId: string, envelopeId: string): MailboxEnvelope | null {
204
- const sessions = loadSessions()
205
- const target = sessions[sessionId]
202
+ const target = loadSession(sessionId)
206
203
  if (!target) throw new Error(`Session not found: ${sessionId}`)
207
- const list = normalizeMailbox(target)
208
- const idx = list.findIndex((env) => env.id === envelopeId)
209
- if (idx === -1) return null
210
- list[idx] = {
211
- ...list[idx],
212
- status: 'ack',
213
- ackAt: Date.now(),
214
- }
215
- target.mailbox = list
216
- target.lastActiveAt = Date.now()
217
- sessions[sessionId] = target
218
- saveSessions(sessions)
219
- return list[idx]
204
+ const ackAt = Date.now()
205
+ let acked: MailboxEnvelope | null = null
206
+ patchSession(sessionId, (current) => {
207
+ if (!current) return null
208
+ const list = normalizeMailbox(current)
209
+ const idx = list.findIndex((env) => env.id === envelopeId)
210
+ if (idx === -1) return current
211
+ list[idx] = {
212
+ ...list[idx],
213
+ status: 'ack',
214
+ ackAt,
215
+ }
216
+ acked = list[idx]
217
+ return {
218
+ ...current,
219
+ mailbox: list,
220
+ lastActiveAt: ackAt,
221
+ }
222
+ })
223
+ return acked
220
224
  }
221
225
 
222
226
  export function clearMailbox(sessionId: string, includeAcked = true): { before: number; after: number } {
223
- const sessions = loadSessions()
224
- const target = sessions[sessionId]
227
+ const target = loadSession(sessionId)
225
228
  if (!target) throw new Error(`Session not found: ${sessionId}`)
226
- const list = normalizeMailbox(target)
227
- const before = list.length
228
- const afterList = includeAcked ? [] : list.filter((env) => env.status !== 'ack')
229
- target.mailbox = afterList
230
- target.lastActiveAt = Date.now()
231
- sessions[sessionId] = target
232
- saveSessions(sessions)
233
- return { before, after: afterList.length }
229
+ let before = 0
230
+ let after = 0
231
+ patchSession(sessionId, (current) => {
232
+ if (!current) return null
233
+ const list = normalizeMailbox(current)
234
+ const afterList = includeAcked ? [] : list.filter((env) => env.status !== 'ack')
235
+ before = list.length
236
+ after = afterList.length
237
+ return {
238
+ ...current,
239
+ mailbox: afterList,
240
+ lastActiveAt: Date.now(),
241
+ }
242
+ })
243
+ return { before, after }
234
244
  }
235
245
 
236
246
  export function bridgeHumanReplyFromChat(input: {