@swarmclawai/swarmclaw 1.2.1 → 1.2.3

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 (149) hide show
  1. package/README.md +16 -85
  2. package/bin/server-cmd.js +64 -1
  3. package/package.json +2 -2
  4. package/skills/coding-agent/SKILL.md +111 -0
  5. package/skills/github/SKILL.md +140 -0
  6. package/skills/nano-banana-pro/SKILL.md +62 -0
  7. package/skills/nano-banana-pro/scripts/generate_image.py +235 -0
  8. package/skills/nano-pdf/SKILL.md +53 -0
  9. package/skills/openai-image-gen/SKILL.md +78 -0
  10. package/skills/openai-image-gen/scripts/gen.py +328 -0
  11. package/skills/resourceful-problem-solving/SKILL.md +49 -0
  12. package/skills/skill-creator/SKILL.md +147 -0
  13. package/skills/skill-creator/scripts/init_skill.py +378 -0
  14. package/skills/skill-creator/scripts/quick_validate.py +159 -0
  15. package/skills/summarize/SKILL.md +77 -0
  16. package/src/app/api/auth/route.ts +20 -5
  17. package/src/app/api/chats/[id]/devserver/route.ts +13 -19
  18. package/src/app/api/chats/[id]/messages/route.ts +13 -15
  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/ip/route.ts +2 -2
  24. package/src/app/api/preview-server/route.ts +1 -1
  25. package/src/app/api/projects/[id]/route.ts +7 -46
  26. package/src/cli/server-cmd.test.js +74 -0
  27. package/src/components/chat/chat-area.tsx +45 -23
  28. package/src/components/chat/message-bubble.test.ts +35 -0
  29. package/src/components/chat/message-bubble.tsx +19 -9
  30. package/src/components/chat/message-list.tsx +37 -3
  31. package/src/components/input/chat-input.tsx +34 -14
  32. package/src/components/openclaw/openclaw-deploy-panel.tsx +4 -0
  33. package/src/instrumentation.ts +1 -1
  34. package/src/lib/chat/assistant-render-id.ts +3 -0
  35. package/src/lib/chat/chat-streaming-state.test.ts +42 -3
  36. package/src/lib/chat/chat-streaming-state.ts +20 -8
  37. package/src/lib/chat/queued-message-queue.test.ts +23 -1
  38. package/src/lib/chat/queued-message-queue.ts +11 -2
  39. package/src/lib/providers/cli-utils.test.ts +124 -0
  40. package/src/lib/server/activity/activity-log.ts +21 -0
  41. package/src/lib/server/agents/agent-availability.test.ts +10 -5
  42. package/src/lib/server/agents/agent-cascade.ts +79 -59
  43. package/src/lib/server/agents/agent-registry.ts +3 -1
  44. package/src/lib/server/agents/agent-repository.ts +90 -0
  45. package/src/lib/server/agents/delegation-job-repository.ts +53 -0
  46. package/src/lib/server/agents/delegation-jobs.ts +11 -4
  47. package/src/lib/server/agents/guardian-checkpoint-repository.ts +35 -0
  48. package/src/lib/server/agents/guardian.ts +2 -2
  49. package/src/lib/server/agents/main-agent-loop.ts +10 -3
  50. package/src/lib/server/agents/main-loop-state-repository.ts +38 -0
  51. package/src/lib/server/agents/subagent-runtime.ts +9 -6
  52. package/src/lib/server/agents/subagent-swarm.ts +3 -2
  53. package/src/lib/server/agents/task-session.ts +3 -4
  54. package/src/lib/server/approvals/approval-repository.ts +30 -0
  55. package/src/lib/server/autonomy/supervisor-incident-repository.ts +42 -0
  56. package/src/lib/server/chat-execution/chat-execution-types.ts +38 -0
  57. package/src/lib/server/chat-execution/chat-execution-utils.ts +1 -1
  58. package/src/lib/server/chat-execution/chat-execution.ts +84 -1926
  59. package/src/lib/server/chat-execution/chat-turn-finalization.ts +620 -0
  60. package/src/lib/server/chat-execution/chat-turn-partial-persistence.ts +221 -0
  61. package/src/lib/server/chat-execution/chat-turn-preflight.ts +133 -0
  62. package/src/lib/server/chat-execution/chat-turn-preparation.ts +817 -0
  63. package/src/lib/server/chat-execution/chat-turn-stream-execution.ts +296 -0
  64. package/src/lib/server/chat-execution/chat-turn-tool-routing.ts +5 -5
  65. package/src/lib/server/chat-execution/message-classifier.test.ts +329 -0
  66. package/src/lib/server/chat-execution/post-stream-finalization.ts +1 -1
  67. package/src/lib/server/chat-execution/prompt-builder.ts +11 -0
  68. package/src/lib/server/chat-execution/prompt-sections.ts +5 -6
  69. package/src/lib/server/chat-execution/situational-awareness.ts +12 -7
  70. package/src/lib/server/chat-execution/stream-agent-chat.ts +16 -13
  71. package/src/lib/server/chatrooms/chatroom-repository.ts +32 -0
  72. package/src/lib/server/connectors/connector-repository.ts +58 -0
  73. package/src/lib/server/connectors/runtime-state.test.ts +117 -0
  74. package/src/lib/server/credentials/credential-repository.ts +7 -0
  75. package/src/lib/server/gateways/gateway-profile-repository.ts +4 -0
  76. package/src/lib/server/memory/memory-abstract.test.ts +59 -0
  77. package/src/lib/server/missions/mission-repository.ts +74 -0
  78. package/src/lib/server/missions/mission-service/actions.ts +6 -0
  79. package/src/lib/server/missions/mission-service/bindings.ts +9 -0
  80. package/src/lib/server/missions/mission-service/context.ts +4 -0
  81. package/src/lib/server/missions/mission-service/core.ts +2269 -0
  82. package/src/lib/server/missions/mission-service/queries.ts +12 -0
  83. package/src/lib/server/missions/mission-service/recovery.ts +5 -0
  84. package/src/lib/server/missions/mission-service/ticks.ts +9 -0
  85. package/src/lib/server/missions/mission-service.test.ts +9 -2
  86. package/src/lib/server/missions/mission-service.ts +6 -2266
  87. package/src/lib/server/openclaw/deploy.test.ts +42 -3
  88. package/src/lib/server/openclaw/deploy.ts +26 -12
  89. package/src/lib/server/persistence/repository-utils.ts +154 -0
  90. package/src/lib/server/persistence/storage-context.ts +51 -0
  91. package/src/lib/server/persistence/transaction.ts +1 -0
  92. package/src/lib/server/projects/project-repository.ts +36 -0
  93. package/src/lib/server/projects/project-service.ts +79 -0
  94. package/src/lib/server/protocols/protocol-normalization.test.ts +6 -4
  95. package/src/lib/server/runtime/alert-dispatch.ts +1 -1
  96. package/src/lib/server/runtime/daemon-policy.ts +1 -1
  97. package/src/lib/server/runtime/daemon-state/core.ts +1570 -0
  98. package/src/lib/server/runtime/daemon-state/health.ts +6 -0
  99. package/src/lib/server/runtime/daemon-state/policy.ts +7 -0
  100. package/src/lib/server/runtime/daemon-state/supervisor.ts +6 -0
  101. package/src/lib/server/runtime/daemon-state.test.ts +48 -0
  102. package/src/lib/server/runtime/daemon-state.ts +3 -1470
  103. package/src/lib/server/runtime/estop-repository.ts +4 -0
  104. package/src/lib/server/runtime/estop.ts +3 -1
  105. package/src/lib/server/runtime/heartbeat-service.test.ts +2 -2
  106. package/src/lib/server/runtime/heartbeat-service.ts +55 -34
  107. package/src/lib/server/runtime/heartbeat-wake.ts +6 -4
  108. package/src/lib/server/runtime/idle-window.ts +2 -2
  109. package/src/lib/server/runtime/network.ts +11 -0
  110. package/src/lib/server/runtime/orchestrator-events.ts +2 -2
  111. package/src/lib/server/runtime/queue/claims.ts +4 -0
  112. package/src/lib/server/runtime/queue/core.ts +2079 -0
  113. package/src/lib/server/runtime/queue/execution.ts +7 -0
  114. package/src/lib/server/runtime/queue/followups.ts +4 -0
  115. package/src/lib/server/runtime/queue/queries.ts +12 -0
  116. package/src/lib/server/runtime/queue/recovery.ts +7 -0
  117. package/src/lib/server/runtime/queue-recovery.test.ts +48 -13
  118. package/src/lib/server/runtime/queue-repository.ts +17 -0
  119. package/src/lib/server/runtime/queue.ts +5 -2061
  120. package/src/lib/server/runtime/run-ledger.ts +6 -5
  121. package/src/lib/server/runtime/run-repository.ts +73 -0
  122. package/src/lib/server/runtime/runtime-lock-repository.ts +8 -0
  123. package/src/lib/server/runtime/runtime-settings.ts +1 -1
  124. package/src/lib/server/runtime/runtime-state.ts +99 -0
  125. package/src/lib/server/runtime/scheduler.ts +4 -2
  126. package/src/lib/server/runtime/session-run-manager/cancellation.ts +157 -0
  127. package/src/lib/server/runtime/session-run-manager/drain.ts +246 -0
  128. package/src/lib/server/runtime/session-run-manager/enqueue.ts +287 -0
  129. package/src/lib/server/runtime/session-run-manager/queries.ts +117 -0
  130. package/src/lib/server/runtime/session-run-manager/recovery.ts +238 -0
  131. package/src/lib/server/runtime/session-run-manager/state.ts +441 -0
  132. package/src/lib/server/runtime/session-run-manager/types.ts +74 -0
  133. package/src/lib/server/runtime/session-run-manager.ts +72 -1377
  134. package/src/lib/server/runtime/watch-job-repository.ts +35 -0
  135. package/src/lib/server/runtime/watch-jobs.ts +3 -1
  136. package/src/lib/server/schedules/schedule-repository.ts +42 -0
  137. package/src/lib/server/sessions/session-repository.ts +85 -0
  138. package/src/lib/server/settings/settings-repository.ts +25 -0
  139. package/src/lib/server/skills/skill-discovery.test.ts +2 -2
  140. package/src/lib/server/skills/skill-discovery.ts +2 -2
  141. package/src/lib/server/skills/skill-repository.ts +14 -0
  142. package/src/lib/server/storage.ts +13 -24
  143. package/src/lib/server/tasks/task-repository.ts +54 -0
  144. package/src/lib/server/usage/usage-repository.ts +30 -0
  145. package/src/lib/server/webhooks/webhook-repository.ts +10 -0
  146. package/src/lib/strip-internal-metadata.test.ts +42 -41
  147. package/src/stores/use-chat-store.test.ts +54 -0
  148. package/src/stores/use-chat-store.ts +21 -5
  149. /package/{bundled-skills → skills}/google-workspace/SKILL.md +0 -0
@@ -73,13 +73,24 @@ function buildExtensionCapabilityLines(enabledExtensions: string[], opts?: { del
73
73
  return lines
74
74
  }
75
75
 
76
+ const DISPLAY_TOOL_ALIASES: Record<string, string[]> = {
77
+ files: ['send_file'],
78
+ shell: ['sandbox_exec', 'sandbox_list_runtimes'],
79
+ }
80
+
76
81
  function buildExactToolNameList(enabledExtensions: string[]): string[] {
77
82
  const planning = getEnabledToolPlanningView(enabledExtensions)
83
+ const displayAliases = dedup(
84
+ enabledExtensions
85
+ .map((toolId) => canonicalizeExtensionId(toolId))
86
+ .flatMap((toolId) => DISPLAY_TOOL_ALIASES[toolId] || []),
87
+ )
78
88
  const extensionToolNames = getExtensionManager()
79
89
  .getTools(enabledExtensions)
80
90
  .map(({ tool }) => tool.name)
81
91
  const combined = [
82
92
  ...planning.displayToolIds,
93
+ ...displayAliases,
83
94
  ...planning.entries.map((entry) => entry.toolName),
84
95
  ...extensionToolNames,
85
96
  ]
@@ -9,7 +9,8 @@
9
9
  import type { Session, Agent } from '@/types'
10
10
  import type { ActiveProjectContext } from '@/lib/server/project-context'
11
11
  import { buildIdentityContinuityContext } from '@/lib/server/identity-continuity'
12
- import { loadSkills, loadAgents } from '@/lib/server/storage'
12
+ import { getAgent, listAgents } from '@/lib/server/agents/agent-repository'
13
+ import { loadSkills } from '@/lib/server/skills/skill-repository'
13
14
  import { buildRuntimeSkillPromptBlocks, resolveRuntimeSkills } from '@/lib/server/skills/runtime-skill-resolver'
14
15
  import { resolveTeam } from '@/lib/server/agents/team-resolution'
15
16
 
@@ -127,8 +128,7 @@ export async function buildAgentAwarenessSection(
127
128
  // Load agent to get delegation settings so the awareness block respects them
128
129
  let delegationOpts: { delegationTargetMode?: 'all' | 'selected'; delegationTargetAgentIds?: string[] } | undefined
129
130
  try {
130
- const agents = loadAgents() as Record<string, Agent>
131
- const agent = agents[session.agentId]
131
+ const agent = getAgent(session.agentId)
132
132
  if (agent?.delegationTargetMode === 'selected') {
133
133
  delegationOpts = {
134
134
  delegationTargetMode: 'selected',
@@ -334,7 +334,7 @@ export function buildCoordinatorSection(
334
334
  ): string | null {
335
335
  if (!agent || agent.role !== 'coordinator') return null
336
336
 
337
- const allAgents = loadAgents()
337
+ const allAgents = listAgents()
338
338
  const selfId = agent.id
339
339
 
340
340
  // Resolve which agents this coordinator can delegate to
@@ -511,8 +511,7 @@ export function buildCliDelegationContext(opts: {
511
511
  // Team roster summary
512
512
  if (opts.agent?.id) {
513
513
  try {
514
- const agents = loadAgents() as Record<string, Agent>
515
- const team = resolveTeam(opts.agent.id, agents)
514
+ const team = resolveTeam(opts.agent.id, listAgents())
516
515
  if (team.mode === 'team') {
517
516
  const teammates = [
518
517
  ...(team.coordinator ? [`${team.coordinator.name} (coordinator)`] : []),
@@ -1,4 +1,11 @@
1
- import { loadTasks, loadSchedules, loadSupervisorIncidents, loadMission, loadAgents, loadConnectors, loadChatrooms, loadUsage } from '@/lib/server/storage'
1
+ import { listAgentIncidents } from '@/lib/server/autonomy/supervisor-incident-repository'
2
+ import { listAgents } from '@/lib/server/agents/agent-repository'
3
+ import { loadChatrooms } from '@/lib/server/chatrooms/chatroom-repository'
4
+ import { loadConnectors } from '@/lib/server/connectors/connector-repository'
5
+ import { loadMission } from '@/lib/server/missions/mission-repository'
6
+ import { loadSchedules } from '@/lib/server/schedules/schedule-repository'
7
+ import { loadTasks } from '@/lib/server/tasks/task-repository'
8
+ import { loadUsage } from '@/lib/server/usage/usage-repository'
2
9
  import { listPersistedRuns } from '@/lib/server/runtime/run-ledger'
3
10
  import type { BoardTask, Mission, Schedule, SupervisorIncident, SessionRunRecord } from '@/types'
4
11
 
@@ -258,7 +265,7 @@ export function buildPlatformStatusSummary(): string {
258
265
  const oneDayAgo = now - 86_400_000
259
266
 
260
267
  // Agents
261
- const agents = Object.values(loadAgents())
268
+ const agents = Object.values(listAgents())
262
269
  const activeAgents = agents.filter((a) => a.lastUsedAt && a.lastUsedAt > oneHourAgo)
263
270
 
264
271
  // Tasks
@@ -276,7 +283,7 @@ export function buildPlatformStatusSummary(): string {
276
283
  const connectors = Object.values(loadConnectors())
277
284
  const connectorLines: string[] = []
278
285
  for (const c of connectors) {
279
- const status = c.status === 'running' || c.status === 'connected' ? '✓' : `✗ (${c.status})`
286
+ const status = c.status === 'running' ? '✓' : `✗ (${c.status})`
280
287
  connectorLines.push(`${c.platform || c.id} ${status}`)
281
288
  }
282
289
 
@@ -285,8 +292,7 @@ export function buildPlatformStatusSummary(): string {
285
292
  const activeChatrooms = chatrooms.filter((c) => !c.archivedAt && !c.temporary)
286
293
 
287
294
  // Incidents
288
- const allIncidents = Object.values(loadSupervisorIncidents())
289
- const recentIncidents = allIncidents.filter((i) => i.createdAt > oneDayAgo)
295
+ const recentIncidents = listAgentIncidents().filter((i) => i.createdAt > oneDayAgo)
290
296
  const warnings = recentIncidents.filter((i) => i.severity === 'medium').length
291
297
  const errors = recentIncidents.filter((i) => i.severity === 'high').length
292
298
 
@@ -348,8 +354,7 @@ export function buildSituationalAwarenessBlock(input: SituationalAwarenessInput)
348
354
 
349
355
  const failedRuns = listPersistedRuns({ sessionId, status: 'failed', limit: 10 })
350
356
 
351
- const allIncidents = loadSupervisorIncidents()
352
- const incidents = Object.values(allIncidents).filter((i) => i.agentId === agentId)
357
+ const incidents = listAgentIncidents(agentId)
353
358
 
354
359
  const mission = missionId ? loadMission(missionId) : null
355
360
 
@@ -3,9 +3,10 @@ import { HumanMessage, AIMessage } from '@langchain/core/messages'
3
3
  import { createReactAgent } from '@langchain/langgraph/prebuilt'
4
4
  import { MemorySaver } from '@langchain/langgraph'
5
5
  import { DEFAULT_HEARTBEAT_INTERVAL_SEC } from '@/lib/runtime/heartbeat-defaults'
6
+ import { getAgent } from '@/lib/server/agents/agent-repository'
6
7
  import { buildSessionTools } from '@/lib/server/session-tools'
7
8
  import { buildChatModel } from '@/lib/server/build-llm'
8
- import { loadSettings, loadAgents } from '@/lib/server/storage'
9
+ import { loadSettings } from '@/lib/server/settings/settings-repository'
9
10
  import { getExtensionManager } from '@/lib/server/extensions'
10
11
  import {
11
12
  collectCapabilityAgentContext,
@@ -228,13 +229,14 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
228
229
  // fallbackCredentialIds is intentionally accepted for compatibility with caller signatures.
229
230
  void fallbackCredentialIds
230
231
 
232
+ const sessionAgent = session.agentId ? getAgent(session.agentId) : null
233
+
231
234
  // Resolve agent's thinking level for provider-native params
232
235
  let agentThinkingLevel: 'minimal' | 'low' | 'medium' | 'high' | undefined
233
236
  if (session.thinkingLevel) {
234
237
  agentThinkingLevel = session.thinkingLevel
235
- } else if (session.agentId) {
236
- const agentsForThinking = loadAgents()
237
- agentThinkingLevel = agentsForThinking[session.agentId]?.thinkingLevel
238
+ } else if (sessionAgent) {
239
+ agentThinkingLevel = sessionAgent.thinkingLevel
238
240
  }
239
241
 
240
242
  const llm = buildChatModel({
@@ -320,9 +322,8 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
320
322
  let agentResponseStyle: 'concise' | 'normal' | 'detailed' | null = null
321
323
  let agentResponseMaxChars: number | null = null
322
324
  const activeProjectContext = resolveActiveProjectContext(session)
323
- if (session.agentId) {
324
- const agents = loadAgents()
325
- const agent = agents[session.agentId]
325
+ if (sessionAgent) {
326
+ const agent = sessionAgent
326
327
  isCoordinatorAgent = agent?.role === 'coordinator'
327
328
  agentDelegationEnabled = agent?.delegationEnabled === true
328
329
  agentDelegationTargetMode = agent?.delegationTargetMode === 'selected' ? 'selected' : 'all'
@@ -415,10 +416,8 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
415
416
 
416
417
  // Proactive memory recall — full mode only
417
418
  {
418
- const agents = loadAgents()
419
- const agentForMemory = session.agentId ? agents[session.agentId] : null
420
419
  const memoryResult = await buildProactiveMemorySection(
421
- session, agentForMemory, message, activeProjectContext.projectRoot,
420
+ session, sessionAgent, message, activeProjectContext.projectRoot,
422
421
  isMinimalPrompt, currentThreadRecallRequest,
423
422
  )
424
423
  if (memoryResult.section) promptParts.push(memoryResult.section)
@@ -926,14 +925,18 @@ async function streamAgentChatCore(opts: StreamAgentChatOpts): Promise<StreamAge
926
925
  && !abortController.signal.aborted)
927
926
  || (isTransientProviderError && !abortController.signal.aborted)
928
927
 
929
- const logLevel = abortController.signal.aborted ? 'warn' : 'error'
930
- console[logLevel](`[stream-agent-chat] Error in streamEvents iteration=${iteration}`, {
928
+ const logPayload = {
931
929
  errName, errMsg, errStack,
932
930
  statusCode, retryAfterMs: extractedRetryAfterMs,
933
931
  isRecursionError, isContextOverflow, isTransientAbort,
934
932
  hasToolCalls: state.hasToolCalls, fullTextLen: state.fullText.length,
935
933
  parentAborted: abortController.signal.aborted,
936
- })
934
+ }
935
+ if (abortController.signal.aborted) {
936
+ log.warn(TAG, `Error in streamEvents iteration=${iteration}`, logPayload)
937
+ } else {
938
+ log.error(TAG, `Error in streamEvents iteration=${iteration}`, logPayload)
939
+ }
937
940
 
938
941
  if (timers.requiredToolKickoffTimedOut && limits.canContinue('required_tool') && !abortController.signal.aborted) {
939
942
  const hadPartialOutput = state.fullText.length > iterationStartState.fullText.length || state.streamedToolEvents.length > iterationStartState.toolEventCount
@@ -0,0 +1,32 @@
1
+ import type { Chatroom } from '@/types'
2
+
3
+ import {
4
+ loadChatroom as loadStoredChatroom,
5
+ loadChatrooms as loadStoredChatrooms,
6
+ saveChatrooms as saveStoredChatrooms,
7
+ upsertChatroom as upsertStoredChatroom,
8
+ } from '@/lib/server/storage'
9
+ import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
10
+
11
+ export const chatroomRepository = createRecordRepository<Chatroom>(
12
+ 'chatrooms',
13
+ {
14
+ get(id) {
15
+ return loadStoredChatroom(id) as Chatroom | null
16
+ },
17
+ list() {
18
+ return loadStoredChatrooms() as Record<string, Chatroom>
19
+ },
20
+ upsert(id, value) {
21
+ upsertStoredChatroom(id, value as Chatroom)
22
+ },
23
+ replace(data) {
24
+ saveStoredChatrooms(data)
25
+ },
26
+ },
27
+ )
28
+
29
+ export const loadChatrooms = () => chatroomRepository.list()
30
+ export const loadChatroom = (id: string) => chatroomRepository.get(id)
31
+ export const saveChatrooms = (items: Record<string, Chatroom | Record<string, unknown>>) => chatroomRepository.replace(items as Record<string, Chatroom>)
32
+ export const upsertChatroom = (id: string, value: Chatroom | Record<string, unknown>) => chatroomRepository.upsert(id, value as Chatroom)
@@ -0,0 +1,58 @@
1
+ import type { Connector } from '@/types'
2
+
3
+ import {
4
+ deleteStoredItem,
5
+ loadConnectorHealth as loadStoredConnectorHealth,
6
+ loadConnectors as loadStoredConnectors,
7
+ loadStoredItem,
8
+ saveConnectors as saveStoredConnectors,
9
+ upsertConnectorHealthEvent as upsertStoredConnectorHealthEvent,
10
+ upsertStoredItem,
11
+ } from '@/lib/server/storage'
12
+ import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
13
+
14
+ export const connectorRepository = createRecordRepository<Connector>(
15
+ 'connectors',
16
+ {
17
+ get(id) {
18
+ return loadStoredItem('connectors', id) as Connector | null
19
+ },
20
+ list() {
21
+ return loadStoredConnectors() as Record<string, Connector>
22
+ },
23
+ upsert(id, value) {
24
+ upsertStoredItem('connectors', id, value)
25
+ },
26
+ replace(data) {
27
+ saveStoredConnectors(data)
28
+ },
29
+ patch(id, updater) {
30
+ const current = loadStoredItem('connectors', id) as Connector | null
31
+ const next = updater(current)
32
+ if (next === null) {
33
+ deleteStoredItem('connectors', id)
34
+ return null
35
+ }
36
+ upsertStoredItem('connectors', id, next)
37
+ return next
38
+ },
39
+ delete(id) {
40
+ deleteStoredItem('connectors', id)
41
+ },
42
+ },
43
+ )
44
+
45
+ export const loadConnectors = () => connectorRepository.list()
46
+ export const loadConnector = (id: string) => connectorRepository.get(id)
47
+ export const saveConnectors = (items: Record<string, Connector | Record<string, unknown>>) => connectorRepository.replace(items as Record<string, Connector>)
48
+ export const upsertConnector = (id: string, value: Connector | Record<string, unknown>) => connectorRepository.upsert(id, value as Connector)
49
+ export const patchConnector = (id: string, updater: (current: Connector | null) => Connector | null) => connectorRepository.patch(id, updater)
50
+ export const deleteConnector = (id: string) => connectorRepository.delete(id)
51
+
52
+ export function loadConnectorHealth() {
53
+ return loadStoredConnectorHealth()
54
+ }
55
+
56
+ export function upsertConnectorHealthEvent(id: string, value: Record<string, unknown>) {
57
+ upsertStoredConnectorHealthEvent(id, value)
58
+ }
@@ -0,0 +1,117 @@
1
+ import assert from 'node:assert/strict'
2
+ import { describe, it, before, after } from 'node:test'
3
+
4
+ const originalEnv = {
5
+ SWARMCLAW_BUILD_MODE: process.env.SWARMCLAW_BUILD_MODE,
6
+ }
7
+
8
+ let mod: typeof import('@/lib/server/connectors/runtime-state')
9
+
10
+ before(async () => {
11
+ process.env.SWARMCLAW_BUILD_MODE = '1'
12
+ mod = await import('@/lib/server/connectors/runtime-state')
13
+ })
14
+
15
+ after(() => {
16
+ if (originalEnv.SWARMCLAW_BUILD_MODE === undefined) delete process.env.SWARMCLAW_BUILD_MODE
17
+ else process.env.SWARMCLAW_BUILD_MODE = originalEnv.SWARMCLAW_BUILD_MODE
18
+ })
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // getConnectorRuntimeState
22
+ // ---------------------------------------------------------------------------
23
+
24
+ describe('getConnectorRuntimeState', () => {
25
+ it('returns object with all expected Map keys', () => {
26
+ const state = mod.getConnectorRuntimeState()
27
+ assert.ok(state.running instanceof Map)
28
+ assert.ok(state.lastInboundChannelByConnector instanceof Map)
29
+ assert.ok(state.lastInboundTimeByConnector instanceof Map)
30
+ assert.ok(state.locks instanceof Map)
31
+ assert.ok(state.generationCounter instanceof Map)
32
+ assert.ok(state.scheduledFollowups instanceof Map)
33
+ assert.ok(state.recentInboundByKey instanceof Map)
34
+ assert.ok(state.pendingInboundDebounce instanceof Map)
35
+ assert.ok(state.scheduledFollowupByDedupe instanceof Map)
36
+ assert.ok(state.reconnectStates instanceof Map)
37
+ assert.ok(state.recentOutbound instanceof Map)
38
+ assert.ok(state.routeMessageHandlerRef)
39
+ assert.equal(typeof state.routeMessageHandlerRef.current, 'function')
40
+ })
41
+
42
+ it('returns the same instance on repeated calls (singleton)', () => {
43
+ const a = mod.getConnectorRuntimeState()
44
+ const b = mod.getConnectorRuntimeState()
45
+ assert.equal(a, b)
46
+ })
47
+ })
48
+
49
+ // ---------------------------------------------------------------------------
50
+ // pruneConnectorTrackingState
51
+ // ---------------------------------------------------------------------------
52
+
53
+ describe('pruneConnectorTrackingState', () => {
54
+ it('removes stale entries and retains live ones', () => {
55
+ const state = mod.getConnectorRuntimeState()
56
+
57
+ // Seed tracking maps with stale + live IDs
58
+ state.lastInboundChannelByConnector.set('live-1', 'channel-a')
59
+ state.lastInboundChannelByConnector.set('stale-1', 'channel-b')
60
+ state.lastInboundTimeByConnector.set('live-1', Date.now())
61
+ state.lastInboundTimeByConnector.set('stale-2', Date.now())
62
+ state.generationCounter.set('live-1', 5)
63
+ state.generationCounter.set('stale-1', 3)
64
+
65
+ const liveIds = new Set(['live-1'])
66
+ const removed = mod.pruneConnectorTrackingState(liveIds)
67
+
68
+ // Verify stale entries are gone
69
+ assert.equal(state.lastInboundChannelByConnector.has('stale-1'), false)
70
+ assert.equal(state.lastInboundTimeByConnector.has('stale-2'), false)
71
+ assert.equal(state.generationCounter.has('stale-1'), false)
72
+
73
+ // Verify live entries are retained
74
+ assert.equal(state.lastInboundChannelByConnector.get('live-1'), 'channel-a')
75
+ assert.ok(state.lastInboundTimeByConnector.has('live-1'))
76
+ assert.equal(state.generationCounter.get('live-1'), 5)
77
+
78
+ // Removed count: stale-1 from channel + stale-2 from time + stale-1 from gen = 3
79
+ assert.equal(removed, 3)
80
+ })
81
+
82
+ it('removes all entries when liveIds is empty', () => {
83
+ const state = mod.getConnectorRuntimeState()
84
+
85
+ state.lastInboundChannelByConnector.set('a', 'ch')
86
+ state.lastInboundTimeByConnector.set('a', 1)
87
+ state.generationCounter.set('a', 1)
88
+
89
+ const removed = mod.pruneConnectorTrackingState(new Set())
90
+
91
+ assert.equal(state.lastInboundChannelByConnector.size, 0)
92
+ assert.equal(state.lastInboundTimeByConnector.size, 0)
93
+ assert.equal(state.generationCounter.size, 0)
94
+ assert.ok(removed >= 3)
95
+ })
96
+
97
+ it('returns 0 when all IDs are live', () => {
98
+ const state = mod.getConnectorRuntimeState()
99
+
100
+ // Clear first
101
+ state.lastInboundChannelByConnector.clear()
102
+ state.lastInboundTimeByConnector.clear()
103
+ state.generationCounter.clear()
104
+
105
+ state.lastInboundChannelByConnector.set('x', 'ch')
106
+ state.lastInboundTimeByConnector.set('x', 1)
107
+ state.generationCounter.set('x', 1)
108
+
109
+ const removed = mod.pruneConnectorTrackingState(new Set(['x']))
110
+ assert.equal(removed, 0)
111
+
112
+ // Entries still present
113
+ assert.equal(state.lastInboundChannelByConnector.has('x'), true)
114
+ assert.equal(state.lastInboundTimeByConnector.has('x'), true)
115
+ assert.equal(state.generationCounter.has('x'), true)
116
+ })
117
+ })
@@ -0,0 +1,7 @@
1
+ export {
2
+ decryptKey,
3
+ deleteCredential,
4
+ encryptKey,
5
+ loadCredentials,
6
+ saveCredentials,
7
+ } from '@/lib/server/storage'
@@ -0,0 +1,4 @@
1
+ export {
2
+ loadGatewayProfiles,
3
+ saveGatewayProfiles,
4
+ } from '@/lib/server/storage'
@@ -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
+ })
@@ -0,0 +1,74 @@
1
+ import type { Mission, MissionEvent } from '@/types'
2
+
3
+ import {
4
+ deleteMission as deleteStoredMission,
5
+ loadMission as loadStoredMission,
6
+ loadMissionEvent as loadStoredMissionEvent,
7
+ loadMissionEvents as loadStoredMissionEvents,
8
+ loadMissions as loadStoredMissions,
9
+ patchMission as patchStoredMission,
10
+ saveMissionEvents as saveStoredMissionEvents,
11
+ saveMissions as saveStoredMissions,
12
+ upsertMission as upsertStoredMission,
13
+ upsertMissionEvent as upsertStoredMissionEvent,
14
+ upsertMissionEvents as upsertStoredMissionEvents,
15
+ } from '@/lib/server/storage'
16
+ import { createRecordRepository } from '@/lib/server/persistence/repository-utils'
17
+
18
+ export const missionRepository = createRecordRepository<Mission>(
19
+ 'missions',
20
+ {
21
+ get(id) {
22
+ return loadStoredMission(id) as Mission | null
23
+ },
24
+ list() {
25
+ return loadStoredMissions() as Record<string, Mission>
26
+ },
27
+ upsert(id, value) {
28
+ upsertStoredMission(id, value as Mission)
29
+ },
30
+ replace(data) {
31
+ saveStoredMissions(data as Record<string, Mission>)
32
+ },
33
+ patch(id, updater) {
34
+ return patchStoredMission(id, updater as (current: Mission | null) => Mission | null) as Mission | null
35
+ },
36
+ delete(id) {
37
+ deleteStoredMission(id)
38
+ },
39
+ },
40
+ )
41
+
42
+ export const missionEventRepository = createRecordRepository<MissionEvent>(
43
+ 'mission-events',
44
+ {
45
+ get(id) {
46
+ return loadStoredMissionEvent(id) as MissionEvent | null
47
+ },
48
+ list() {
49
+ return loadStoredMissionEvents() as Record<string, MissionEvent>
50
+ },
51
+ upsert(id, value) {
52
+ upsertStoredMissionEvent(id, value as MissionEvent)
53
+ },
54
+ upsertMany(entries) {
55
+ upsertStoredMissionEvents(entries as Array<[string, MissionEvent]>)
56
+ },
57
+ replace(data) {
58
+ saveStoredMissionEvents(data as Record<string, MissionEvent>)
59
+ },
60
+ },
61
+ )
62
+
63
+ export const loadMissions = () => missionRepository.list()
64
+ export const loadMission = (id: string) => missionRepository.get(id)
65
+ export const saveMissions = (items: Record<string, Mission | Record<string, unknown>>) => missionRepository.replace(items as Record<string, Mission>)
66
+ export const upsertMission = (id: string, value: Mission | Record<string, unknown>) => missionRepository.upsert(id, value as Mission)
67
+ export const patchMission = (id: string, updater: (current: Mission | null) => Mission | null) => missionRepository.patch(id, updater)
68
+ export const deleteMission = (id: string) => missionRepository.delete(id)
69
+
70
+ export const loadMissionEvents = () => missionEventRepository.list()
71
+ export const loadMissionEvent = (id: string) => missionEventRepository.get(id)
72
+ export const saveMissionEvents = (items: Record<string, MissionEvent | Record<string, unknown>>) => missionEventRepository.replace(items as Record<string, MissionEvent>)
73
+ export const upsertMissionEvent = (id: string, value: MissionEvent | Record<string, unknown>) => missionEventRepository.upsert(id, value as MissionEvent)
74
+ export const upsertMissionEvents = (entries: Array<[string, MissionEvent | Record<string, unknown>]>) => missionEventRepository.upsertMany(entries as Array<[string, MissionEvent]>)
@@ -0,0 +1,6 @@
1
+ export {
2
+ appendMissionEvent,
3
+ ensureDelegationMission,
4
+ performMissionAction,
5
+ syncDelegationMissionFromJob,
6
+ } from './core'
@@ -0,0 +1,9 @@
1
+ export {
2
+ bindMissionToSession,
3
+ bindMissionToTask,
4
+ ensureMissionForSchedule,
5
+ ensureMissionForTask,
6
+ noteMissionTaskFinished,
7
+ noteMissionTaskStarted,
8
+ noteScheduleMissionTriggered,
9
+ } from './core'
@@ -0,0 +1,4 @@
1
+ export {
2
+ buildMissionContextBlock,
3
+ buildMissionHeartbeatPrompt,
4
+ } from './core'