@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
@@ -5,13 +5,20 @@ import { genId } from '@/lib/id'
5
5
  import type { Agent, Session, Skill, SkillSuggestion } from '@/types'
6
6
  import { errorMessage } from '@/lib/shared-utils'
7
7
  import {
8
- loadAgents,
9
- loadSessions,
8
+ loadAgent,
9
+ } from '@/lib/server/agents/agent-repository'
10
+ import {
11
+ loadSession,
12
+ } from '@/lib/server/sessions/session-repository'
13
+ import { getMessages, getRecentMessages } from '@/lib/server/messages/message-repository'
14
+ import {
15
+ loadSkill,
16
+ loadSkillSuggestion,
10
17
  loadSkillSuggestions,
11
18
  loadSkills,
12
- saveSkillSuggestions,
13
- saveSkills,
14
- } from '@/lib/server/storage'
19
+ saveSkill,
20
+ upsertSkillSuggestion,
21
+ } from '@/lib/server/skills/skill-repository'
15
22
  import { buildLLM, type GenerationModelPreference } from '@/lib/server/build-llm'
16
23
  import { notify } from '@/lib/server/ws-hub'
17
24
  import { resolveAgentRouteCandidates } from '@/lib/server/agents/agent-runtime-config'
@@ -91,7 +98,7 @@ function ensureHeading(name: string, content: string): string {
91
98
  }
92
99
 
93
100
  export function buildSessionTranscript(session: Session, maxMessages = DEFAULT_TRANSCRIPT_MESSAGES): string {
94
- const messages = Array.isArray(session.messages) ? session.messages.slice(-maxMessages) : []
101
+ const messages = getRecentMessages(session.id, maxMessages)
95
102
  const lines: string[] = []
96
103
  for (const message of messages) {
97
104
  if (!message || message.suppressed) continue
@@ -109,9 +116,8 @@ export function buildSessionTranscript(session: Session, maxMessages = DEFAULT_T
109
116
  }
110
117
 
111
118
  function getSessionMessageCount(session: Session): number {
112
- return Array.isArray(session.messages)
113
- ? session.messages.filter((message) => message && !message.suppressed && (message.text || message.toolEvents?.length)).length
114
- : 0
119
+ return getMessages(session.id)
120
+ .filter((message) => message && !message.suppressed && (message.text || message.toolEvents?.length)).length
115
121
  }
116
122
 
117
123
  function buildSuggestionPrompt(params: {
@@ -234,11 +240,9 @@ export async function createSkillSuggestionFromSession(
234
240
  sessionId: string,
235
241
  options?: { generateText?: (prompt: string) => Promise<string> },
236
242
  ): Promise<SkillSuggestion> {
237
- const sessions = loadSessions()
238
- const session = sessions[sessionId] as unknown as Session | undefined
243
+ const session = loadSession(sessionId)
239
244
  if (!session) throw new Error(`Session "${sessionId}" not found.`)
240
- const agents = loadAgents()
241
- const agent = session.agentId ? agents[session.agentId] : null
245
+ const agent = session.agentId ? loadAgent(session.agentId) : null
242
246
 
243
247
  const transcript = buildSessionTranscript(session)
244
248
  const sourceMessageCount = getSessionMessageCount(session)
@@ -331,18 +335,16 @@ export async function createSkillSuggestionFromSession(
331
335
  updatedAt: now,
332
336
  }
333
337
 
334
- suggestions[suggestion.id] = suggestion
335
- saveSkillSuggestions(suggestions)
338
+ upsertSkillSuggestion(suggestion.id, suggestion)
336
339
  notify('skill_suggestions')
337
340
  return suggestion
338
341
  }
339
342
 
340
343
  export function materializeSkillSuggestion(id: string): { suggestion: SkillSuggestion; skill: Skill } {
341
- const suggestions = loadSkillSuggestions()
342
- const suggestion = suggestions[id]
344
+ const suggestion = loadSkillSuggestion(id)
343
345
  if (!suggestion) throw new Error(`Skill suggestion "${id}" not found.`)
344
346
  if (suggestion.status === 'approved' && suggestion.createdSkillId) {
345
- const existing = loadSkills()[suggestion.createdSkillId]
347
+ const existing = loadSkill(suggestion.createdSkillId)
346
348
  if (existing) return { suggestion, skill: existing }
347
349
  }
348
350
 
@@ -370,8 +372,7 @@ export function materializeSkillSuggestion(id: string): { suggestion: SkillSugge
370
372
  approvedAt: now,
371
373
  updatedAt: now,
372
374
  }
373
- suggestions[id] = approved
374
- saveSkillSuggestions(suggestions)
375
+ upsertSkillSuggestion(id, approved)
375
376
  notify('skill_suggestions')
376
377
  return { suggestion: approved, skill: existingSkill }
377
378
  }
@@ -399,8 +400,7 @@ export function materializeSkillSuggestion(id: string): { suggestion: SkillSugge
399
400
  updatedAt: now,
400
401
  }
401
402
 
402
- skills[skill.id] = skill
403
- saveSkills(skills)
403
+ saveSkill(skill.id, skill)
404
404
  clearDiscoveredSkillsCache()
405
405
 
406
406
  const approved: SkillSuggestion = {
@@ -410,16 +410,14 @@ export function materializeSkillSuggestion(id: string): { suggestion: SkillSugge
410
410
  approvedAt: now,
411
411
  updatedAt: now,
412
412
  }
413
- suggestions[id] = approved
414
- saveSkillSuggestions(suggestions)
413
+ upsertSkillSuggestion(id, approved)
415
414
  notify('skills')
416
415
  notify('skill_suggestions')
417
416
  return { suggestion: approved, skill }
418
417
  }
419
418
 
420
419
  export function rejectSkillSuggestion(id: string): SkillSuggestion {
421
- const suggestions = loadSkillSuggestions()
422
- const suggestion = suggestions[id]
420
+ const suggestion = loadSkillSuggestion(id)
423
421
  if (!suggestion) throw new Error(`Skill suggestion "${id}" not found.`)
424
422
  const rejected: SkillSuggestion = {
425
423
  ...suggestion,
@@ -427,8 +425,7 @@ export function rejectSkillSuggestion(id: string): SkillSuggestion {
427
425
  rejectedAt: Date.now(),
428
426
  updatedAt: Date.now(),
429
427
  }
430
- suggestions[id] = rejected
431
- saveSkillSuggestions(suggestions)
428
+ upsertSkillSuggestion(id, rejected)
432
429
  notify('skill_suggestions')
433
430
  return rejected
434
431
  }
@@ -1,270 +1,47 @@
1
1
  import assert from 'node:assert/strict'
2
- import { after, before, describe, it } from 'node:test'
3
-
4
- let normalizeStoredRecord: typeof import('@/lib/server/storage-normalization').normalizeStoredRecord
5
-
6
- const noopLoader = () => null
7
-
8
- before(async () => {
9
- process.env.SWARMCLAW_BUILD_MODE = '1'
10
- const mod = await import('@/lib/server/storage-normalization')
11
- normalizeStoredRecord = mod.normalizeStoredRecord
12
- })
13
-
14
- after(() => {
15
- delete process.env.SWARMCLAW_BUILD_MODE
16
- })
17
-
18
- describe('storage-normalization', () => {
19
- // ---- Agent normalization ----
20
- describe('agents', () => {
21
- it('adds default capabilities, delegation, role, orgChart fields', () => {
22
- const agent = { id: 'a1', name: 'Test' } as Record<string, unknown>
23
- const result = normalizeStoredRecord('agents', agent, noopLoader).value as Record<string, unknown>
24
- assert.deepEqual(result.capabilities, [])
25
- assert.equal(result.role, 'worker')
26
- assert.equal(result.delegationEnabled, false)
27
- assert.equal(result.orgChart, null)
28
- assert.ok(Array.isArray(result.tools))
29
- assert.ok(Array.isArray(result.extensions))
30
- })
31
-
32
- it('preserves existing values', () => {
33
- const agent = {
34
- id: 'a1',
35
- name: 'Test',
36
- capabilities: ['web', 'code'],
37
- role: 'coordinator',
38
- delegationEnabled: true,
39
- delegationTargetMode: 'all',
40
- delegationTargetAgentIds: [],
41
- orgChart: { parentId: 'p1', teamLabel: 'Eng', teamColor: '#fff', x: 10, y: 20 },
42
- } as Record<string, unknown>
43
- const result = normalizeStoredRecord('agents', agent, noopLoader).value as Record<string, unknown>
44
- assert.deepEqual(result.capabilities, ['web', 'code'])
45
- assert.equal(result.role, 'coordinator')
46
- assert.equal(result.delegationEnabled, true)
47
- })
48
-
49
- it('coordinator always has delegation enabled', () => {
50
- const agent = { id: 'a1', name: 'Test', role: 'coordinator', delegationEnabled: false } as Record<string, unknown>
51
- const result = normalizeStoredRecord('agents', agent, noopLoader).value as Record<string, unknown>
52
- assert.equal(result.delegationEnabled, true)
53
- })
54
-
55
- it('migrates legacy platformAssignScope', () => {
56
- const agent = { id: 'a1', name: 'Test', platformAssignScope: 'all' } as Record<string, unknown>
57
- const result = normalizeStoredRecord('agents', agent, noopLoader).value as Record<string, unknown>
58
- assert.equal(result.delegationEnabled, true)
59
- assert.equal(result.platformAssignScope, undefined)
60
- })
61
-
62
- it('migrates legacy subAgentIds to delegationTargetAgentIds', () => {
63
- const agent = { id: 'a1', name: 'Test', subAgentIds: ['b1', 'b2'] } as Record<string, unknown>
64
- const result = normalizeStoredRecord('agents', agent, noopLoader).value as Record<string, unknown>
65
- assert.deepEqual(result.delegationTargetAgentIds, ['b1', 'b2'])
66
- assert.equal(result.delegationTargetMode, 'selected')
67
- assert.equal(result.subAgentIds, undefined)
68
- })
69
-
70
- it('deletes legacy plugins field', () => {
71
- const agent = { id: 'a1', name: 'Test', plugins: ['old'] } as Record<string, unknown>
72
- const result = normalizeStoredRecord('agents', agent, noopLoader).value as Record<string, unknown>
73
- assert.equal(result.plugins, undefined)
74
- })
75
- })
76
-
77
- // ---- Session normalization ----
78
- describe('sessions', () => {
79
- it('type migration orchestrated → delegated', () => {
80
- const session = { id: 's1', sessionType: 'orchestrated' } as Record<string, unknown>
81
- const result = normalizeStoredRecord('sessions', session, noopLoader).value as Record<string, unknown>
82
- assert.equal(result.sessionType, 'delegated')
83
- })
84
-
85
- it('defaults invalid sessionType to human', () => {
86
- const session = { id: 's1', sessionType: 'bogus' } as Record<string, unknown>
87
- const result = normalizeStoredRecord('sessions', session, noopLoader).value as Record<string, unknown>
88
- assert.equal(result.sessionType, 'human')
89
- })
90
-
91
- it('detects shortcut session from id prefix', () => {
92
- const session = { id: 'agent-thread-abc', agentId: 'a1', sessionType: 'human' } as Record<string, unknown>
93
- const result = normalizeStoredRecord('sessions', session, noopLoader).value as Record<string, unknown>
94
- assert.equal(result.shortcutForAgentId, 'a1')
95
- })
96
-
97
- it('normalizes capabilities', () => {
98
- const session = { id: 's1', sessionType: 'human', tools: ['web'] } as Record<string, unknown>
99
- const result = normalizeStoredRecord('sessions', session, noopLoader).value as Record<string, unknown>
100
- assert.ok(Array.isArray(result.tools))
101
- assert.ok(Array.isArray(result.extensions))
102
- })
103
-
104
- it('deletes legacy plugins and mainLoopState', () => {
105
- const session = { id: 's1', sessionType: 'human', plugins: ['old'], mainLoopState: {} } as Record<string, unknown>
106
- const result = normalizeStoredRecord('sessions', session, noopLoader).value as Record<string, unknown>
107
- assert.equal(result.plugins, undefined)
108
- assert.equal(result.mainLoopState, undefined)
109
- })
110
- })
111
-
112
- // ---- Schedule normalization ----
113
- describe('schedules', () => {
114
- it('type resolution from scheduleType', () => {
115
- const schedule = { id: 'sch1', scheduleType: 'cron', status: 'active' } as Record<string, unknown>
116
- const result = normalizeStoredRecord('schedules', schedule, noopLoader).value as Record<string, unknown>
117
- assert.equal(result.scheduleType, 'cron')
118
- })
119
-
120
- it('legacy type field migrated', () => {
121
- const schedule = { id: 'sch1', type: 'once', status: 'active' } as Record<string, unknown>
122
- const result = normalizeStoredRecord('schedules', schedule, noopLoader).value as Record<string, unknown>
123
- assert.equal(result.scheduleType, 'once')
124
- assert.equal(result.type, undefined)
125
- })
126
-
127
- it('invalid status defaults to active', () => {
128
- const schedule = { id: 'sch1', status: 'bogus' } as Record<string, unknown>
129
- const result = normalizeStoredRecord('schedules', schedule, noopLoader).value as Record<string, unknown>
130
- assert.equal(result.status, 'active')
131
- })
132
-
133
- it('timestamp parsing from number', () => {
134
- const schedule = { id: 'sch1', status: 'active', lastRunAt: 1700000000000 } as Record<string, unknown>
135
- const result = normalizeStoredRecord('schedules', schedule, noopLoader).value as Record<string, unknown>
136
- assert.equal(result.lastRunAt, 1700000000000)
137
- })
138
- })
139
-
140
- // ---- Task normalization ----
141
- describe('tasks', () => {
142
- it('subtaskIds default to empty array', () => {
143
- const task = { id: 't1', title: 'Test' } as Record<string, unknown>
144
- const result = normalizeStoredRecord('tasks', task, noopLoader).value as Record<string, unknown>
145
- assert.deepEqual(result.subtaskIds, [])
146
- })
147
-
148
- it('preserves existing subtaskIds', () => {
149
- const task = { id: 't1', title: 'Test', subtaskIds: ['t2'] } as Record<string, unknown>
150
- const result = normalizeStoredRecord('tasks', task, noopLoader).value as Record<string, unknown>
151
- assert.deepEqual(result.subtaskIds, ['t2'])
152
- })
153
-
154
- it('removes missionSummary field', () => {
155
- const task = { id: 't1', missionSummary: 'old' } as Record<string, unknown>
156
- const result = normalizeStoredRecord('tasks', task, noopLoader).value as Record<string, unknown>
157
- assert.equal(result.missionSummary, undefined)
158
- })
159
- })
160
-
161
- describe('provider_configs', () => {
162
- it('defaults legacy custom provider configs to enabled with normalized fields', () => {
163
- const providerConfig = {
164
- id: 'custom-llama',
165
- name: ' Llama.cpp ',
166
- type: 'custom',
167
- baseUrl: ' http://localhost:8080/v1/ ',
168
- models: [' llama-3.1-70b ', 'llama-3.1-70b', ''],
169
- } as Record<string, unknown>
170
- const result = normalizeStoredRecord('provider_configs', providerConfig, noopLoader).value as Record<string, unknown>
171
- assert.equal(result.name, 'Llama.cpp')
172
- assert.equal(result.baseUrl, 'http://localhost:8080/v1/')
173
- assert.deepEqual(result.models, ['llama-3.1-70b'])
174
- assert.equal(result.requiresApiKey, true)
175
- assert.equal(result.isEnabled, true)
176
- assert.equal(result.credentialId, null)
177
- })
178
-
179
- it('normalizes builtin override configs without treating them as custom providers', () => {
180
- const providerConfig = {
181
- id: 'openai',
182
- type: 'builtin',
183
- isEnabled: false,
184
- } as Record<string, unknown>
185
- const result = normalizeStoredRecord('provider_configs', providerConfig, noopLoader).value as Record<string, unknown>
186
- assert.equal(result.type, 'builtin')
187
- assert.equal(result.name, 'Built-in Provider')
188
- assert.equal(result.baseUrl, '')
189
- assert.deepEqual(result.models, [])
190
- assert.equal(result.requiresApiKey, true)
191
- assert.equal(result.isEnabled, false)
192
- assert.equal(result.credentialId, null)
193
- })
194
-
195
- it('defaults createdAt and updatedAt for legacy records missing timestamps', () => {
196
- const providerConfig = {
197
- id: 'custom-old',
198
- name: 'Old Provider',
199
- type: 'custom',
200
- baseUrl: 'http://localhost:8080/v1',
201
- models: ['model-a'],
202
- } as Record<string, unknown>
203
- const result = normalizeStoredRecord('provider_configs', providerConfig, noopLoader).value as Record<string, unknown>
204
- assert.equal(typeof result.createdAt, 'number')
205
- assert.equal(typeof result.updatedAt, 'number')
206
- assert.ok((result.createdAt as number) > 0)
207
- assert.equal(result.updatedAt, result.createdAt)
208
- })
209
- })
210
-
211
- // ---- Mission normalization ----
212
- describe('missions', () => {
213
- it('defaults status/phase/sourceRef', () => {
214
- const mission = { id: 'm1' } as Record<string, unknown>
215
- const result = normalizeStoredRecord('missions', mission, noopLoader).value as Record<string, unknown>
216
- assert.equal(result.status, 'active')
217
- assert.equal(result.phase, 'planning')
218
- assert.deepEqual(result.sourceRef, { kind: 'manual' })
219
- })
220
-
221
- it('preserves valid status and phase', () => {
222
- const mission = { id: 'm1', status: 'waiting', phase: 'executing' } as Record<string, unknown>
223
- const result = normalizeStoredRecord('missions', mission, noopLoader).value as Record<string, unknown>
224
- assert.equal(result.status, 'waiting')
225
- assert.equal(result.phase, 'executing')
226
- })
227
-
228
- it('sourceRef from sessionId', () => {
229
- const mission = { id: 'm1', sessionId: 'sess-1' } as Record<string, unknown>
230
- const result = normalizeStoredRecord('missions', mission, noopLoader).value as Record<string, unknown>
231
- assert.deepEqual(result.sourceRef, { kind: 'chat', sessionId: 'sess-1' })
232
- })
233
-
234
- it('waitState defaults', () => {
235
- const mission = {
236
- id: 'm1',
237
- waitState: { kind: 'approval', reason: 'Needs sign-off' },
238
- } as Record<string, unknown>
239
- const result = normalizeStoredRecord('missions', mission, noopLoader).value as Record<string, unknown>
240
- const ws = result.waitState as Record<string, unknown>
241
- assert.equal(ws.kind, 'approval')
242
- assert.equal(ws.reason, 'Needs sign-off')
243
- })
244
- })
245
-
246
- // ---- DelegationJob normalization ----
247
- describe('delegation_jobs', () => {
248
- it('missionId cleanup', () => {
249
- const job = { id: 'j1', missionId: ' m1 ' } as Record<string, unknown>
250
- const result = normalizeStoredRecord('delegation_jobs', job, noopLoader).value as Record<string, unknown>
251
- assert.equal(result.missionId, 'm1')
252
- })
253
-
254
- it('empty missionId is deleted', () => {
255
- const job = { id: 'j1', missionId: ' ' } as Record<string, unknown>
256
- const result = normalizeStoredRecord('delegation_jobs', job, noopLoader).value as Record<string, unknown>
257
- assert.equal(result.missionId, undefined)
258
- })
259
- })
260
-
261
- // ---- Unknown collection ----
262
- describe('unknown collection', () => {
263
- it('passes through unchanged', () => {
264
- const data = { id: 'x1', foo: 'bar' }
265
- const result = normalizeStoredRecord('widgets', data, noopLoader)
266
- assert.deepEqual(result.value, data)
267
- assert.equal(result.changed, false)
268
- })
2
+ import { describe, it } from 'node:test'
3
+
4
+ import { normalizeStoredRecord } from '@/lib/server/storage-normalization'
5
+
6
+ const loadItem = () => null
7
+
8
+ describe('storage normalization for runtime execution records', () => {
9
+ it('backfills execution metadata on legacy runtime runs', () => {
10
+ const { value, changed } = normalizeStoredRecord('runtime_runs', {
11
+ id: 'run_1',
12
+ sessionId: 'sess_1',
13
+ source: 'task',
14
+ internal: false,
15
+ mode: 'followup',
16
+ status: 'queued',
17
+ messagePreview: 'Build the feature',
18
+ queuedAt: 123,
19
+ }, loadItem)
20
+
21
+ const record = value as Record<string, unknown>
22
+ assert.equal(changed, true)
23
+ assert.equal(record.kind, 'session_turn')
24
+ assert.equal(record.ownerType, 'session')
25
+ assert.equal(record.ownerId, 'sess_1')
26
+ assert.equal(record.parentExecutionId, null)
27
+ assert.equal(record.recoveryPolicy, 'restart_recoverable')
28
+ })
29
+
30
+ it('backfills execution metadata on legacy runtime run events', () => {
31
+ const { value, changed } = normalizeStoredRecord('runtime_run_events', {
32
+ id: 'evt_1',
33
+ runId: 'run_1',
34
+ sessionId: 'sess_1',
35
+ timestamp: 123,
36
+ phase: 'status',
37
+ event: { t: 'md', text: '{}' },
38
+ }, loadItem)
39
+
40
+ const record = value as Record<string, unknown>
41
+ assert.equal(changed, true)
42
+ assert.equal(record.kind, 'session_turn')
43
+ assert.equal(record.ownerType, 'session')
44
+ assert.equal(record.ownerId, 'sess_1')
45
+ assert.equal(record.parentExecutionId, null)
269
46
  })
270
47
  })
@@ -369,6 +369,47 @@ function normalizeStoredDelegationJobRecord(value: unknown): unknown {
369
369
  return job
370
370
  }
371
371
 
372
+ function normalizeStoredRuntimeRunRecord(value: unknown): unknown {
373
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value
374
+ const run = value as StoredObject
375
+
376
+ if (typeof run.kind !== 'string' || !run.kind.trim()) run.kind = 'session_turn'
377
+ if (run.ownerType === undefined) run.ownerType = 'session'
378
+ if (run.ownerId === undefined) {
379
+ const sessionId = typeof run.sessionId === 'string' && run.sessionId.trim() ? run.sessionId.trim() : ''
380
+ run.ownerId = sessionId || null
381
+ }
382
+ if (run.parentExecutionId === undefined) run.parentExecutionId = null
383
+ if (run.recoveryPolicy === undefined) {
384
+ const source = typeof run.source === 'string' ? run.source.trim().toLowerCase() : ''
385
+ run.recoveryPolicy = source === 'heartbeat'
386
+ || source === 'heartbeat-wake'
387
+ || source === 'schedule'
388
+ || source === 'task'
389
+ || source === 'delegation'
390
+ || source === 'subagent'
391
+ ? 'restart_recoverable'
392
+ : 'ephemeral'
393
+ }
394
+
395
+ return run
396
+ }
397
+
398
+ function normalizeStoredRuntimeRunEventRecord(value: unknown): unknown {
399
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return value
400
+ const event = value as StoredObject
401
+
402
+ if (typeof event.kind !== 'string' || !event.kind.trim()) event.kind = 'session_turn'
403
+ if (event.ownerType === undefined) event.ownerType = 'session'
404
+ if (event.ownerId === undefined) {
405
+ const sessionId = typeof event.sessionId === 'string' && event.sessionId.trim() ? event.sessionId.trim() : ''
406
+ event.ownerId = sessionId || null
407
+ }
408
+ if (event.parentExecutionId === undefined) event.parentExecutionId = null
409
+
410
+ return event
411
+ }
412
+
372
413
  // --- Main dispatch function ---
373
414
 
374
415
  export interface NormalizationResult {
@@ -393,6 +434,7 @@ export function normalizeStoredRecord(
393
434
  && table !== 'mission_events' && table !== 'delegation_jobs'
394
435
  && table !== 'schedules' && table !== 'sessions'
395
436
  && table !== 'provider_configs'
437
+ && table !== 'runtime_runs' && table !== 'runtime_run_events'
396
438
  ) {
397
439
  return { value, changed: false }
398
440
  }
@@ -517,6 +559,14 @@ function normalizeStoredRecordInner(
517
559
  return normalizeStoredDelegationJobRecord(value)
518
560
  }
519
561
 
562
+ if (table === 'runtime_runs') {
563
+ return normalizeStoredRuntimeRunRecord(value)
564
+ }
565
+
566
+ if (table === 'runtime_run_events') {
567
+ return normalizeStoredRuntimeRunEventRecord(value)
568
+ }
569
+
520
570
  if (table === 'schedules') {
521
571
  return normalizeStoredScheduleRecord(value, loadItem)
522
572
  }
@@ -547,11 +597,36 @@ function normalizeStoredRecordInner(
547
597
  if ('plugins' in session) delete session.plugins
548
598
  if ('mainLoopState' in session) delete session.mainLoopState
549
599
  if ('missionSummary' in session) delete session.missionSummary
600
+ // Messages are now stored in session_messages table — ensure default empty array
601
+ if (!Array.isArray(session.messages)) session.messages = []
602
+ // Default messageCount for pre-migration blobs
603
+ if (typeof session.messageCount !== 'number') {
604
+ session.messageCount = (session.messages as unknown[]).length
605
+ }
550
606
  // Default geminiSessionId for new field
551
607
  if (session.geminiSessionId === undefined) session.geminiSessionId = null
552
608
  // Default injectedMemoryIds for proactive recall dedup
553
609
  if (!session.injectedMemoryIds || typeof session.injectedMemoryIds !== 'object') {
554
610
  session.injectedMemoryIds = {}
555
611
  }
612
+ // Validate runContext if present — leave null/undefined alone (created on demand)
613
+ if (session.runContext != null) {
614
+ if (typeof session.runContext !== 'object' || Array.isArray(session.runContext)) {
615
+ session.runContext = null
616
+ } else {
617
+ const rc = session.runContext as Record<string, unknown>
618
+ if (typeof rc.objective !== 'string' && rc.objective !== null) rc.objective = null
619
+ if (!Array.isArray(rc.constraints)) rc.constraints = []
620
+ if (!Array.isArray(rc.keyFacts)) rc.keyFacts = []
621
+ if (!Array.isArray(rc.discoveries)) rc.discoveries = []
622
+ if (!Array.isArray(rc.failedApproaches)) rc.failedApproaches = []
623
+ if (!Array.isArray(rc.currentPlan)) rc.currentPlan = []
624
+ if (!Array.isArray(rc.completedSteps)) rc.completedSteps = []
625
+ if (!Array.isArray(rc.blockers)) rc.blockers = []
626
+ if (typeof rc.parentContext !== 'string' && rc.parentContext !== null) rc.parentContext = null
627
+ if (typeof rc.updatedAt !== 'number') rc.updatedAt = Date.now()
628
+ if (typeof rc.version !== 'number') rc.version = 0
629
+ }
630
+ }
556
631
  return session
557
632
  }
@@ -100,6 +100,9 @@ export function withTransaction<T>(fn: () => T): T {
100
100
  return wrapped()
101
101
  }
102
102
 
103
+ /** Internal: raw database handle for specialized repositories (e.g. message-repository). */
104
+ export function getDb(): InstanceType<typeof Database> { return db }
105
+
103
106
  type StoredObject = Record<string, unknown>
104
107
  type StoredSessionRecord = Session
105
108
  type StoredAgentRecord = Agent
@@ -157,6 +160,8 @@ const COLLECTIONS = [
157
160
  'provider_health',
158
161
  'swarm_snapshots',
159
162
  'main_loop_states',
163
+ 'working_states',
164
+ 'daemon_status',
160
165
  ] as const
161
166
 
162
167
  export type StorageCollection = (typeof COLLECTIONS)[number]
@@ -180,6 +185,14 @@ db.exec(`CREATE TABLE IF NOT EXISTS runtime_locks (
180
185
  updated_at INTEGER NOT NULL
181
186
  )`)
182
187
 
188
+ // Relational message storage — messages extracted from session blobs (Phase 1)
189
+ db.exec(`CREATE TABLE IF NOT EXISTS session_messages (
190
+ session_id TEXT NOT NULL,
191
+ seq INTEGER NOT NULL,
192
+ data TEXT NOT NULL,
193
+ PRIMARY KEY (session_id, seq)
194
+ ) WITHOUT ROWID`)
195
+
183
196
  // --- Internal normalize helper that binds the loadItem dependency ---
184
197
  function normalize(table: string, value: unknown): NormalizationResult {
185
198
  return normalizeStoredRecord(table, value, loadCollectionItem)
@@ -1594,6 +1607,12 @@ export const loadPersistedMainLoopState = mainLoopStatesStore.loadItem
1594
1607
  export const upsertPersistedMainLoopState = mainLoopStatesStore.upsert
1595
1608
  export const deletePersistedMainLoopState = mainLoopStatesStore.deleteItem
1596
1609
 
1610
+ // --- Working States ---
1611
+ const workingStatesStore = createCollectionStore('working_states')
1612
+ export const loadPersistedWorkingState = workingStatesStore.loadItem
1613
+ export const upsertPersistedWorkingState = workingStatesStore.upsert
1614
+ export const deletePersistedWorkingState = workingStatesStore.deleteItem
1615
+
1597
1616
  export function getSessionMessages(sessionId: string): Message[] {
1598
1617
  const session = loadSession(sessionId)
1599
1618
  return Array.isArray(session?.messages) ? session.messages : []
@@ -1,6 +1,6 @@
1
1
  import type { Session } from '@/types'
2
2
  import { getProvider, streamChatWithFailover } from '@/lib/providers'
3
- import { decryptKey, loadCredentials } from './storage'
3
+ import { requireCredentialSecret, resolveCredentialSecret } from './credentials/credential-service'
4
4
  import { extractDocumentArtifact, type DocumentArtifact } from './document-utils'
5
5
 
6
6
  type JsonSchemaLike = Record<string, unknown>
@@ -31,21 +31,10 @@ function resolveApiKey(session: ExtractionSession): string | null {
31
31
  if (!provider) throw new Error(`Unknown provider: ${session.provider}`)
32
32
  if (provider.requiresApiKey) {
33
33
  if (!session.credentialId) throw new Error('No API key configured for this session')
34
- const creds = loadCredentials()
35
- const cred = creds[session.credentialId]
36
- if (!cred?.encryptedKey) throw new Error('API key not found. Please add one in Settings.')
37
- return decryptKey(cred.encryptedKey)
34
+ return requireCredentialSecret(session.credentialId, 'API key not found. Please add one in Settings.')
38
35
  }
39
36
  if (provider.optionalApiKey && session.credentialId) {
40
- const creds = loadCredentials()
41
- const cred = creds[session.credentialId]
42
- if (cred?.encryptedKey) {
43
- try {
44
- return decryptKey(cred.encryptedKey)
45
- } catch {
46
- return null
47
- }
48
- }
37
+ return resolveCredentialSecret(session.credentialId)
49
38
  }
50
39
  return null
51
40
  }