@swarmclawai/swarmclaw 0.7.8 → 0.8.0

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 (251) hide show
  1. package/README.md +12 -15
  2. package/next.config.ts +13 -2
  3. package/package.json +4 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +9 -0
  5. package/src/app/api/agents/route.ts +4 -0
  6. package/src/app/api/agents/thread-route.test.ts +133 -0
  7. package/src/app/api/approvals/route.test.ts +148 -0
  8. package/src/app/api/canvas/[sessionId]/route.ts +3 -1
  9. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -2
  10. package/src/app/api/chats/[id]/devserver/route.ts +48 -7
  11. package/src/app/api/chats/[id]/messages/route.ts +42 -18
  12. package/src/app/api/chats/[id]/route.ts +1 -1
  13. package/src/app/api/chats/[id]/stop/route.ts +5 -4
  14. package/src/app/api/chats/route.ts +22 -2
  15. package/src/app/api/clawhub/install/route.ts +28 -8
  16. package/src/app/api/connectors/[id]/route.ts +26 -1
  17. package/src/app/api/external-agents/route.test.ts +165 -0
  18. package/src/app/api/gateways/[id]/health/route.ts +27 -12
  19. package/src/app/api/gateways/[id]/route.ts +2 -0
  20. package/src/app/api/gateways/health-route.test.ts +135 -0
  21. package/src/app/api/gateways/route.ts +2 -0
  22. package/src/app/api/mcp-servers/route.test.ts +130 -0
  23. package/src/app/api/openclaw/deploy/route.ts +38 -5
  24. package/src/app/api/plugins/install/route.ts +46 -6
  25. package/src/app/api/plugins/marketplace/route.ts +48 -15
  26. package/src/app/api/preview-server/route.ts +26 -11
  27. package/src/app/api/schedules/[id]/run/route.ts +4 -0
  28. package/src/app/api/schedules/route.test.ts +86 -0
  29. package/src/app/api/schedules/route.ts +6 -1
  30. package/src/app/api/setup/check-provider/route.test.ts +19 -0
  31. package/src/app/api/setup/check-provider/route.ts +40 -10
  32. package/src/app/api/skills/[id]/route.ts +12 -0
  33. package/src/app/api/skills/import/route.ts +14 -12
  34. package/src/app/api/skills/route.ts +13 -1
  35. package/src/app/api/tasks/[id]/route.ts +10 -1
  36. package/src/app/api/tasks/import/github/route.test.ts +65 -0
  37. package/src/app/api/tasks/import/github/route.ts +337 -0
  38. package/src/app/api/wallets/[id]/approve/route.ts +17 -3
  39. package/src/app/api/wallets/[id]/route.ts +79 -33
  40. package/src/app/api/wallets/[id]/send/route.ts +19 -33
  41. package/src/app/api/wallets/route.ts +78 -61
  42. package/src/app/api/webhooks/[id]/route.ts +33 -6
  43. package/src/app/api/webhooks/route.test.ts +272 -0
  44. package/src/cli/index.js +1 -0
  45. package/src/cli/spec.js +1 -0
  46. package/src/components/agents/agent-card.tsx +9 -2
  47. package/src/components/agents/agent-chat-list.tsx +18 -2
  48. package/src/components/agents/agent-list.tsx +1 -0
  49. package/src/components/agents/agent-sheet.tsx +73 -24
  50. package/src/components/agents/inspector-panel.tsx +41 -0
  51. package/src/components/canvas/canvas-panel.tsx +236 -65
  52. package/src/components/chat/chat-card.tsx +36 -13
  53. package/src/components/chat/chat-header.tsx +44 -16
  54. package/src/components/chat/chat-list.tsx +28 -4
  55. package/src/components/chat/checkpoint-timeline.tsx +50 -34
  56. package/src/components/chat/message-bubble.tsx +208 -145
  57. package/src/components/chat/message-list.tsx +48 -19
  58. package/src/components/chatrooms/chatroom-message.tsx +2 -2
  59. package/src/components/chatrooms/chatroom-sheet.tsx +16 -2
  60. package/src/components/connectors/connector-health.tsx +1 -1
  61. package/src/components/connectors/connector-list.tsx +7 -2
  62. package/src/components/connectors/connector-sheet.tsx +337 -148
  63. package/src/components/gateways/gateway-sheet.tsx +2 -2
  64. package/src/components/mcp-servers/mcp-server-list.tsx +26 -5
  65. package/src/components/mcp-servers/mcp-server-sheet.tsx +19 -2
  66. package/src/components/openclaw/openclaw-deploy-panel.tsx +269 -21
  67. package/src/components/plugins/plugin-list.tsx +45 -9
  68. package/src/components/plugins/plugin-sheet.tsx +55 -7
  69. package/src/components/providers/provider-list.tsx +2 -1
  70. package/src/components/providers/provider-sheet.tsx +21 -2
  71. package/src/components/schedules/schedule-card.tsx +25 -1
  72. package/src/components/schedules/schedule-sheet.tsx +44 -2
  73. package/src/components/secrets/secret-sheet.tsx +21 -2
  74. package/src/components/shared/agent-switch-dialog.tsx +12 -1
  75. package/src/components/shared/bottom-sheet.tsx +13 -3
  76. package/src/components/shared/command-palette.tsx +8 -1
  77. package/src/components/shared/confirm-dialog.tsx +19 -4
  78. package/src/components/shared/connector-platform-icon.test.ts +28 -0
  79. package/src/components/shared/connector-platform-icon.tsx +39 -6
  80. package/src/components/shared/settings/plugin-manager.tsx +29 -6
  81. package/src/components/shared/settings/section-capability-policy.tsx +7 -3
  82. package/src/components/skills/skill-list.tsx +25 -0
  83. package/src/components/skills/skill-sheet.tsx +84 -12
  84. package/src/components/tasks/approvals-panel.tsx +191 -95
  85. package/src/components/tasks/task-board.tsx +273 -2
  86. package/src/components/tasks/task-card.tsx +38 -9
  87. package/src/components/ui/dialog.tsx +2 -2
  88. package/src/components/wallets/wallet-approval-dialog.tsx +4 -2
  89. package/src/components/wallets/wallet-panel.tsx +435 -90
  90. package/src/components/wallets/wallet-section.tsx +198 -48
  91. package/src/components/webhooks/webhook-sheet.tsx +22 -2
  92. package/src/lib/approval-display.ts +20 -0
  93. package/src/lib/canvas-content.ts +198 -0
  94. package/src/lib/chat-artifact-summary.ts +165 -0
  95. package/src/lib/chat-display.test.ts +91 -0
  96. package/src/lib/chat-display.ts +58 -0
  97. package/src/lib/chat-streaming-state.test.ts +47 -1
  98. package/src/lib/chat-streaming-state.ts +42 -0
  99. package/src/lib/ollama-model.ts +10 -0
  100. package/src/lib/openclaw-endpoint.test.ts +8 -0
  101. package/src/lib/openclaw-endpoint.ts +6 -1
  102. package/src/lib/plugin-install-cors.ts +46 -0
  103. package/src/lib/plugin-sources.test.ts +43 -0
  104. package/src/lib/plugin-sources.ts +77 -0
  105. package/src/lib/providers/ollama.ts +16 -6
  106. package/src/lib/providers/openclaw.test.ts +54 -0
  107. package/src/lib/providers/openclaw.ts +127 -11
  108. package/src/lib/schedule-dedupe-advanced.test.ts +1335 -0
  109. package/src/lib/schedule-dedupe.test.ts +66 -1
  110. package/src/lib/schedule-dedupe.ts +169 -12
  111. package/src/lib/schedule-origin.test.ts +20 -0
  112. package/src/lib/schedule-origin.ts +15 -0
  113. package/src/lib/server/__fixtures__/fake-mcp-stdio-server.mjs +27 -0
  114. package/src/lib/server/agent-availability.ts +16 -0
  115. package/src/lib/server/agent-runtime-config.ts +12 -4
  116. package/src/lib/server/agent-thread-session.test.ts +51 -0
  117. package/src/lib/server/agent-thread-session.ts +7 -0
  118. package/src/lib/server/approval-match.ts +205 -0
  119. package/src/lib/server/approvals-auto-approve.test.ts +538 -1
  120. package/src/lib/server/approvals.ts +214 -1
  121. package/src/lib/server/assistant-control.test.ts +29 -0
  122. package/src/lib/server/assistant-control.ts +23 -0
  123. package/src/lib/server/build-llm.test.ts +79 -0
  124. package/src/lib/server/build-llm.ts +14 -4
  125. package/src/lib/server/canvas-content.test.ts +32 -0
  126. package/src/lib/server/canvas-content.ts +6 -0
  127. package/src/lib/server/capability-router.test.ts +11 -0
  128. package/src/lib/server/capability-router.ts +26 -1
  129. package/src/lib/server/chat-execution-advanced.test.ts +651 -0
  130. package/src/lib/server/chat-execution-disabled.test.ts +94 -0
  131. package/src/lib/server/chat-execution-tool-events.test.ts +157 -0
  132. package/src/lib/server/chat-execution.ts +353 -72
  133. package/src/lib/server/clawhub-client.test.ts +14 -8
  134. package/src/lib/server/connectors/manager.test.ts +1147 -0
  135. package/src/lib/server/connectors/manager.ts +362 -63
  136. package/src/lib/server/connectors/pairing.ts +26 -5
  137. package/src/lib/server/connectors/types.ts +2 -0
  138. package/src/lib/server/connectors/whatsapp.test.ts +134 -0
  139. package/src/lib/server/connectors/whatsapp.ts +271 -47
  140. package/src/lib/server/context-manager.ts +6 -1
  141. package/src/lib/server/daemon-state.ts +1 -1
  142. package/src/lib/server/data-dir.test.ts +37 -0
  143. package/src/lib/server/data-dir.ts +20 -1
  144. package/src/lib/server/delegation-jobs-advanced.test.ts +513 -0
  145. package/src/lib/server/devserver-launch.test.ts +60 -0
  146. package/src/lib/server/devserver-launch.ts +85 -0
  147. package/src/lib/server/elevenlabs.test.ts +189 -1
  148. package/src/lib/server/elevenlabs.ts +147 -43
  149. package/src/lib/server/ethereum.ts +590 -0
  150. package/src/lib/server/eval/agent-regression-advanced.test.ts +302 -0
  151. package/src/lib/server/eval/agent-regression.test.ts +18 -1
  152. package/src/lib/server/eval/agent-regression.ts +383 -11
  153. package/src/lib/server/evm-swap.ts +475 -0
  154. package/src/lib/server/execution-log.ts +1 -0
  155. package/src/lib/server/heartbeat-service-timer.test.ts +173 -0
  156. package/src/lib/server/heartbeat-service.ts +15 -10
  157. package/src/lib/server/heartbeat-wake.test.ts +112 -0
  158. package/src/lib/server/heartbeat-wake.ts +338 -57
  159. package/src/lib/server/main-agent-loop-advanced.test.ts +538 -0
  160. package/src/lib/server/mcp-client.test.ts +16 -0
  161. package/src/lib/server/mcp-client.ts +25 -0
  162. package/src/lib/server/memory-integration.test.ts +719 -0
  163. package/src/lib/server/memory-policy.test.ts +43 -0
  164. package/src/lib/server/memory-policy.ts +132 -0
  165. package/src/lib/server/memory-tiers.test.ts +60 -0
  166. package/src/lib/server/memory-tiers.ts +16 -0
  167. package/src/lib/server/ollama-runtime.ts +58 -0
  168. package/src/lib/server/openclaw-deploy.test.ts +109 -1
  169. package/src/lib/server/openclaw-deploy.ts +557 -81
  170. package/src/lib/server/openclaw-gateway.test.ts +131 -0
  171. package/src/lib/server/openclaw-gateway.ts +10 -4
  172. package/src/lib/server/openclaw-health.test.ts +35 -0
  173. package/src/lib/server/openclaw-health.ts +215 -47
  174. package/src/lib/server/orchestrator-lg.ts +2 -2
  175. package/src/lib/server/plugins-advanced.test.ts +351 -0
  176. package/src/lib/server/plugins.ts +205 -5
  177. package/src/lib/server/queue-advanced.test.ts +528 -0
  178. package/src/lib/server/queue-followups.test.ts +262 -0
  179. package/src/lib/server/queue-reconcile.test.ts +128 -0
  180. package/src/lib/server/queue.ts +293 -61
  181. package/src/lib/server/scheduler.ts +29 -1
  182. package/src/lib/server/session-note.test.ts +36 -0
  183. package/src/lib/server/session-note.ts +42 -0
  184. package/src/lib/server/session-run-manager.ts +52 -4
  185. package/src/lib/server/session-tools/canvas.ts +14 -12
  186. package/src/lib/server/session-tools/connector.test.ts +138 -0
  187. package/src/lib/server/session-tools/connector.ts +348 -61
  188. package/src/lib/server/session-tools/context.ts +12 -3
  189. package/src/lib/server/session-tools/crud.ts +221 -10
  190. package/src/lib/server/session-tools/delegate-fallback.test.ts +103 -0
  191. package/src/lib/server/session-tools/delegate.ts +64 -8
  192. package/src/lib/server/session-tools/discovery-approvals.test.ts +142 -0
  193. package/src/lib/server/session-tools/discovery.ts +80 -12
  194. package/src/lib/server/session-tools/file-normalize.test.ts +36 -0
  195. package/src/lib/server/session-tools/file.ts +43 -4
  196. package/src/lib/server/session-tools/human-loop.ts +35 -5
  197. package/src/lib/server/session-tools/index.ts +44 -9
  198. package/src/lib/server/session-tools/manage-connectors.test.ts +139 -0
  199. package/src/lib/server/session-tools/manage-schedules-advanced.test.ts +564 -0
  200. package/src/lib/server/session-tools/manage-schedules.test.ts +283 -0
  201. package/src/lib/server/session-tools/manage-tasks-advanced.test.ts +852 -0
  202. package/src/lib/server/session-tools/memory.test.ts +93 -0
  203. package/src/lib/server/session-tools/memory.ts +546 -79
  204. package/src/lib/server/session-tools/normalize-tool-args.ts +1 -1
  205. package/src/lib/server/session-tools/plugin-creator.ts +57 -1
  206. package/src/lib/server/session-tools/primitive-tools.test.ts +6 -0
  207. package/src/lib/server/session-tools/schedule.ts +6 -1
  208. package/src/lib/server/session-tools/shell-normalize.test.ts +25 -1
  209. package/src/lib/server/session-tools/shell.ts +22 -3
  210. package/src/lib/server/session-tools/wallet-tool.test.ts +254 -0
  211. package/src/lib/server/session-tools/wallet.ts +1374 -139
  212. package/src/lib/server/session-tools/web-inputs.test.ts +162 -1
  213. package/src/lib/server/session-tools/web.ts +468 -64
  214. package/src/lib/server/skill-discovery.ts +128 -0
  215. package/src/lib/server/skill-eligibility.test.ts +84 -0
  216. package/src/lib/server/skill-eligibility.ts +95 -0
  217. package/src/lib/server/skill-prompt-budget.test.ts +102 -0
  218. package/src/lib/server/skill-prompt-budget.ts +125 -0
  219. package/src/lib/server/skills-normalize.test.ts +54 -0
  220. package/src/lib/server/skills-normalize.ts +372 -26
  221. package/src/lib/server/solana.ts +214 -29
  222. package/src/lib/server/storage.ts +65 -36
  223. package/src/lib/server/stream-agent-chat.test.ts +419 -9
  224. package/src/lib/server/stream-agent-chat.ts +887 -83
  225. package/src/lib/server/system-events.ts +1 -1
  226. package/src/lib/server/tool-capability-policy-advanced.test.ts +502 -0
  227. package/src/lib/server/tool-loop-detection.test.ts +105 -0
  228. package/src/lib/server/tool-loop-detection.ts +260 -0
  229. package/src/lib/server/tool-planning.ts +4 -2
  230. package/src/lib/server/wallet-execution.test.ts +198 -0
  231. package/src/lib/server/wallet-portfolio.test.ts +98 -0
  232. package/src/lib/server/wallet-portfolio.ts +724 -0
  233. package/src/lib/server/wallet-service.test.ts +57 -0
  234. package/src/lib/server/wallet-service.ts +213 -0
  235. package/src/lib/server/watch-jobs-advanced.test.ts +594 -0
  236. package/src/lib/server/watch-jobs.ts +17 -2
  237. package/src/lib/server/workspace-context.ts +111 -0
  238. package/src/lib/skill-save-payload.test.ts +39 -0
  239. package/src/lib/skill-save-payload.ts +37 -0
  240. package/src/lib/tasks.ts +28 -0
  241. package/src/lib/tool-event-summary.test.ts +30 -0
  242. package/src/lib/tool-event-summary.ts +37 -0
  243. package/src/lib/validation/schemas.ts +1 -0
  244. package/src/lib/wallet-transactions.test.ts +75 -0
  245. package/src/lib/wallet-transactions.ts +43 -0
  246. package/src/lib/wallet.test.ts +17 -0
  247. package/src/lib/wallet.ts +183 -0
  248. package/src/proxy.test.ts +31 -0
  249. package/src/proxy.ts +34 -2
  250. package/src/stores/use-chat-store.ts +15 -1
  251. package/src/types/index.ts +210 -14
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict'
2
2
  import { test } from 'node:test'
3
- import { findDuplicateSchedule, getScheduleSignatureKey, type ScheduleLike } from './schedule-dedupe.ts'
3
+ import { findDuplicateSchedule, findEquivalentSchedules, getScheduleSignatureKey, type ScheduleLike } from './schedule-dedupe.ts'
4
4
 
5
5
  test('findDuplicateSchedule matches active interval schedules with normalized prompts', () => {
6
6
  const schedules: Record<string, ScheduleLike> = {
@@ -82,3 +82,68 @@ test('getScheduleSignatureKey is stable for equivalent schedules', () => {
82
82
  assert.equal(keyA, keyB)
83
83
  assert.notEqual(keyA, keyC)
84
84
  })
85
+
86
+ test('findDuplicateSchedule fuzzy-matches same-session recurring reminders with different wording and cadence shape', () => {
87
+ const schedules: Record<string, ScheduleLike> = {
88
+ daily1: {
89
+ id: 'daily1',
90
+ agentId: 'assistant',
91
+ taskPrompt: 'Daily check for updates on US-Iran tensions',
92
+ scheduleType: 'cron',
93
+ cron: '0 9 * * *',
94
+ status: 'active',
95
+ createdByAgentId: 'assistant',
96
+ createdInSessionId: 'session-1',
97
+ createdAt: 1,
98
+ },
99
+ }
100
+
101
+ const duplicate = findDuplicateSchedule(schedules, {
102
+ agentId: 'assistant',
103
+ taskPrompt: 'Periodic update check for US-Iran tensions',
104
+ scheduleType: 'interval',
105
+ intervalMs: 86_400_000,
106
+ createdByAgentId: 'assistant',
107
+ createdInSessionId: 'session-1',
108
+ }, {
109
+ creatorScope: {
110
+ agentId: 'assistant',
111
+ sessionId: 'session-1',
112
+ },
113
+ })
114
+
115
+ assert.ok(duplicate)
116
+ assert.equal(duplicate?.id, 'daily1')
117
+ })
118
+
119
+ test('findEquivalentSchedules does not fuzzy-match across sessions', () => {
120
+ const schedules: Record<string, ScheduleLike> = {
121
+ daily1: {
122
+ id: 'daily1',
123
+ agentId: 'assistant',
124
+ taskPrompt: 'Daily check for updates on US-Iran tensions',
125
+ scheduleType: 'cron',
126
+ cron: '0 9 * * *',
127
+ status: 'active',
128
+ createdByAgentId: 'assistant',
129
+ createdInSessionId: 'session-1',
130
+ createdAt: 1,
131
+ },
132
+ }
133
+
134
+ const matches = findEquivalentSchedules(schedules, {
135
+ agentId: 'assistant',
136
+ taskPrompt: 'Periodic update check for US-Iran tensions',
137
+ scheduleType: 'interval',
138
+ intervalMs: 86_400_000,
139
+ createdByAgentId: 'assistant',
140
+ createdInSessionId: 'session-2',
141
+ }, {
142
+ creatorScope: {
143
+ agentId: 'assistant',
144
+ sessionId: 'session-2',
145
+ },
146
+ })
147
+
148
+ assert.deepEqual(matches, [])
149
+ })
@@ -1,3 +1,4 @@
1
+ import { CronExpressionParser } from 'cron-parser'
1
2
  import type { ScheduleType } from '@/types'
2
3
 
3
4
  export type ScheduleLike = {
@@ -41,12 +42,49 @@ interface ScheduleSignature {
41
42
  id: string
42
43
  agentId: string
43
44
  taskPrompt: string
45
+ promptTokens: string[]
44
46
  scheduleType: ScheduleType
45
47
  cron: string
46
48
  intervalMs: number | null
47
49
  runAt: number | null
48
50
  }
49
51
 
52
+ type ScheduleMatchKind = 'exact' | 'fuzzy'
53
+
54
+ const PROMPT_STOPWORDS = new Set([
55
+ 'a',
56
+ 'an',
57
+ 'and',
58
+ 'any',
59
+ 'at',
60
+ 'back',
61
+ 'by',
62
+ 'check',
63
+ 'for',
64
+ 'from',
65
+ 'if',
66
+ 'in',
67
+ 'into',
68
+ 'me',
69
+ 'my',
70
+ 'of',
71
+ 'on',
72
+ 'once',
73
+ 'please',
74
+ 'remind',
75
+ 'report',
76
+ 'task',
77
+ 'the',
78
+ 'this',
79
+ 'to',
80
+ 'up',
81
+ 'update',
82
+ 'updates',
83
+ 'with',
84
+ ])
85
+
86
+ const ONCE_MATCH_WINDOW_MS = 15 * 60 * 1000
87
+
50
88
  function normalizeString(value: unknown): string {
51
89
  return typeof value === 'string' ? value.trim() : ''
52
90
  }
@@ -57,6 +95,26 @@ function normalizePrompt(value: unknown): string {
57
95
  return text.replace(/\s+/g, ' ').trim().toLowerCase()
58
96
  }
59
97
 
98
+ function normalizePromptToken(token: string): string {
99
+ let normalized = token
100
+ if (normalized.length > 4 && normalized.endsWith('ies')) normalized = `${normalized.slice(0, -3)}y`
101
+ else if (normalized.length > 5 && normalized.endsWith('ing')) normalized = normalized.slice(0, -3)
102
+ else if (normalized.length > 4 && normalized.endsWith('ed')) normalized = normalized.slice(0, -2)
103
+ else if (normalized.length > 3 && normalized.endsWith('s') && !normalized.endsWith('ss')) normalized = normalized.slice(0, -1)
104
+ return normalized
105
+ }
106
+
107
+ function tokenizePrompt(value: unknown): string[] {
108
+ const normalized = normalizePrompt(value).replace(/[^a-z0-9]+/g, ' ')
109
+ if (!normalized) return []
110
+ return normalized
111
+ .split(' ')
112
+ .map((token) => normalizePromptToken(token.trim()))
113
+ .filter((token) => token.length > 0)
114
+ .filter((token) => token.length > 2 || ['ai', 'uk', 'us', 'eu'].includes(token))
115
+ .filter((token) => !PROMPT_STOPWORDS.has(token))
116
+ }
117
+
60
118
  function normalizeCron(value: unknown): string {
61
119
  const cron = normalizeString(value)
62
120
  if (!cron) return ''
@@ -84,6 +142,7 @@ function toSignature(raw: ScheduleLike | ScheduleDuplicateCandidate): ScheduleSi
84
142
  id: normalizeString(raw.id),
85
143
  agentId: normalizeString(raw.agentId),
86
144
  taskPrompt: normalizePrompt(raw.taskPrompt),
145
+ promptTokens: tokenizePrompt(raw.taskPrompt),
87
146
  scheduleType: normalizeScheduleType(raw.scheduleType),
88
147
  cron: normalizeCron(raw.cron),
89
148
  intervalMs: normalizePositiveInt(raw.intervalMs),
@@ -116,6 +175,85 @@ function sameCadence(a: ScheduleSignature, b: ScheduleSignature): boolean {
116
175
  return false
117
176
  }
118
177
 
178
+ function tryResolveCronIntervalMs(cron: string): number | null {
179
+ if (!cron) return null
180
+ try {
181
+ const interval = CronExpressionParser.parse(cron, {
182
+ currentDate: new Date('2026-01-01T00:00:00.000Z'),
183
+ })
184
+ const first = interval.next().getTime()
185
+ const second = interval.next().getTime()
186
+ const diff = second - first
187
+ return diff > 0 ? diff : null
188
+ } catch {
189
+ return null
190
+ }
191
+ }
192
+
193
+ function cadenceFamilyFromMs(intervalMs: number | null): string {
194
+ if (intervalMs == null || intervalMs <= 0) return ''
195
+
196
+ const families: Array<{ label: string; ms: number; toleranceMs: number }> = [
197
+ { label: '15m', ms: 15 * 60 * 1000, toleranceMs: 60 * 1000 },
198
+ { label: '30m', ms: 30 * 60 * 1000, toleranceMs: 2 * 60 * 1000 },
199
+ { label: 'hourly', ms: 60 * 60 * 1000, toleranceMs: 5 * 60 * 1000 },
200
+ { label: '6h', ms: 6 * 60 * 60 * 1000, toleranceMs: 15 * 60 * 1000 },
201
+ { label: '12h', ms: 12 * 60 * 60 * 1000, toleranceMs: 30 * 60 * 1000 },
202
+ { label: 'daily', ms: 24 * 60 * 60 * 1000, toleranceMs: 60 * 60 * 1000 },
203
+ { label: 'weekly', ms: 7 * 24 * 60 * 60 * 1000, toleranceMs: 2 * 60 * 60 * 1000 },
204
+ ]
205
+
206
+ for (const family of families) {
207
+ if (Math.abs(intervalMs - family.ms) <= family.toleranceMs) return family.label
208
+ }
209
+
210
+ return `interval:${Math.round(intervalMs / 60_000)}m`
211
+ }
212
+
213
+ function cadenceFamily(signature: ScheduleSignature): string {
214
+ if (signature.scheduleType === 'once') return signature.runAt != null ? 'once' : ''
215
+ if (signature.scheduleType === 'interval') return cadenceFamilyFromMs(signature.intervalMs)
216
+ if (signature.scheduleType === 'cron') return cadenceFamilyFromMs(tryResolveCronIntervalMs(signature.cron))
217
+ return ''
218
+ }
219
+
220
+ function sameCadenceFamily(a: ScheduleSignature, b: ScheduleSignature): boolean {
221
+ if (sameCadence(a, b)) return true
222
+ if (a.scheduleType === 'once' && b.scheduleType === 'once') {
223
+ if (a.runAt == null || b.runAt == null) return false
224
+ return Math.abs(a.runAt - b.runAt) <= ONCE_MATCH_WINDOW_MS
225
+ }
226
+ if (a.scheduleType === 'once' || b.scheduleType === 'once') return false
227
+ const aFamily = cadenceFamily(a)
228
+ const bFamily = cadenceFamily(b)
229
+ return aFamily !== '' && aFamily === bFamily
230
+ }
231
+
232
+ function countTokenOverlap(a: string[], b: string[]): number {
233
+ if (!a.length || !b.length) return 0
234
+ const smaller = a.length <= b.length ? a : b
235
+ const largerSet = new Set(a.length <= b.length ? b : a)
236
+ let overlap = 0
237
+ for (const token of new Set(smaller)) {
238
+ if (largerSet.has(token)) overlap += 1
239
+ }
240
+ return overlap
241
+ }
242
+
243
+ function hasFuzzyPromptMatch(a: ScheduleSignature, b: ScheduleSignature): boolean {
244
+ if (!a.promptTokens.length || !b.promptTokens.length) return false
245
+ const uniqueA = [...new Set(a.promptTokens)]
246
+ const uniqueB = [...new Set(b.promptTokens)]
247
+ const overlap = countTokenOverlap(uniqueA, uniqueB)
248
+ if (overlap === 0) return false
249
+ const smallerSize = Math.min(uniqueA.length, uniqueB.length)
250
+ const largerSize = Math.max(uniqueA.length, uniqueB.length)
251
+ const coverage = overlap / smallerSize
252
+ const jaccard = overlap / new Set([...uniqueA, ...uniqueB]).size
253
+ if (smallerSize <= 2) return overlap === smallerSize
254
+ return overlap >= 2 && coverage >= 0.67 && (jaccard >= 0.5 || overlap >= Math.max(2, largerSize - 1))
255
+ }
256
+
119
257
  function isEligibleStatus(status: unknown, includeStatuses: Set<string>): boolean {
120
258
  const normalized = normalizeString(status).toLowerCase() || 'active'
121
259
  return includeStatuses.has(normalized)
@@ -149,26 +287,45 @@ export function findDuplicateSchedule(
149
287
  candidateRaw: ScheduleDuplicateCandidate,
150
288
  opts: FindDuplicateScheduleOptions = {},
151
289
  ): ScheduleLike | null {
290
+ return findEquivalentSchedules(schedules, candidateRaw, opts)[0] || null
291
+ }
292
+
293
+ export function findEquivalentSchedules(
294
+ schedules: Record<string, ScheduleLike>,
295
+ candidateRaw: ScheduleDuplicateCandidate,
296
+ opts: FindDuplicateScheduleOptions = {},
297
+ ): ScheduleLike[] {
152
298
  const candidate = toSignature(candidateRaw)
153
- if (!candidate.agentId) return null
154
- if (!candidate.taskPrompt) return null
299
+ if (!candidate.agentId) return []
300
+ if (!candidate.taskPrompt) return []
155
301
 
156
302
  const ignoreId = normalizeString(opts.ignoreId || candidate.id)
157
303
  const statuses = new Set((opts.includeStatuses?.length ? opts.includeStatuses : ['active', 'paused']).map((s) => s.toLowerCase()))
304
+ const scopeSessionId = normalizeString(opts.creatorScope?.sessionId)
158
305
 
159
306
  const matches = Object.values(schedules)
160
307
  .filter((existing) => existing && typeof existing === 'object')
161
- .filter((existing) => {
308
+ .map((existing) => {
162
309
  const signature = toSignature(existing)
163
- if (!signature.id) return false
164
- if (ignoreId && signature.id === ignoreId) return false
165
- if (!isEligibleStatus(existing.status, statuses)) return false
166
- if (!matchesCreatorScope(existing, opts.creatorScope || null)) return false
167
- if (signature.agentId !== candidate.agentId) return false
168
- if (signature.taskPrompt !== candidate.taskPrompt) return false
169
- return sameCadence(signature, candidate)
310
+ if (!signature.id) return null
311
+ if (ignoreId && signature.id === ignoreId) return null
312
+ if (!isEligibleStatus(existing.status, statuses)) return null
313
+ if (!matchesCreatorScope(existing, opts.creatorScope || null)) return null
314
+ if (signature.agentId !== candidate.agentId) return null
315
+ const exact = signature.taskPrompt === candidate.taskPrompt && sameCadence(signature, candidate)
316
+ if (exact) return { existing, kind: 'exact' as const }
317
+ const fuzzy = Boolean(scopeSessionId)
318
+ && hasFuzzyPromptMatch(signature, candidate)
319
+ && sameCadenceFamily(signature, candidate)
320
+ if (!fuzzy) return null
321
+ return { existing, kind: 'fuzzy' as const }
322
+ })
323
+ .filter((entry): entry is { existing: ScheduleLike; kind: ScheduleMatchKind } => Boolean(entry))
324
+ .sort((a, b) => {
325
+ if (a.kind !== b.kind) return a.kind === 'exact' ? -1 : 1
326
+ return compareUpdatedDesc(a.existing, b.existing)
170
327
  })
171
- .sort(compareUpdatedDesc)
328
+ .map((entry) => entry.existing)
172
329
 
173
- return matches[0] || null
330
+ return matches
174
331
  }
@@ -0,0 +1,20 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+
4
+ import {
5
+ isAgentCreatedSchedule,
6
+ isUserCreatedSchedule,
7
+ shouldAutoDeleteScheduleAfterTerminalRun,
8
+ } from './schedule-origin'
9
+
10
+ test('recognizes agent-created schedules', () => {
11
+ assert.equal(isAgentCreatedSchedule({ scheduleType: 'interval', createdByAgentId: 'molly-2' }), true)
12
+ assert.equal(isUserCreatedSchedule({ scheduleType: 'interval', createdByAgentId: 'molly-2' }), false)
13
+ })
14
+
15
+ test('recognizes manual schedules and only auto-deletes agent-created one-offs', () => {
16
+ assert.equal(isUserCreatedSchedule({ scheduleType: 'once', createdByAgentId: null }), true)
17
+ assert.equal(shouldAutoDeleteScheduleAfterTerminalRun({ scheduleType: 'once', createdByAgentId: null }), false)
18
+ assert.equal(shouldAutoDeleteScheduleAfterTerminalRun({ scheduleType: 'once', createdByAgentId: 'molly-2' }), true)
19
+ assert.equal(shouldAutoDeleteScheduleAfterTerminalRun({ scheduleType: 'interval', createdByAgentId: 'molly-2' }), false)
20
+ })
@@ -0,0 +1,15 @@
1
+ import type { Schedule } from '@/types'
2
+
3
+ type ScheduleOriginShape = Pick<Schedule, 'scheduleType' | 'createdByAgentId'>
4
+
5
+ export function isAgentCreatedSchedule(schedule: ScheduleOriginShape | null | undefined): boolean {
6
+ return Boolean(typeof schedule?.createdByAgentId === 'string' && schedule.createdByAgentId.trim())
7
+ }
8
+
9
+ export function isUserCreatedSchedule(schedule: ScheduleOriginShape | null | undefined): boolean {
10
+ return !isAgentCreatedSchedule(schedule)
11
+ }
12
+
13
+ export function shouldAutoDeleteScheduleAfterTerminalRun(schedule: ScheduleOriginShape | null | undefined): boolean {
14
+ return Boolean(schedule?.scheduleType === 'once' && isAgentCreatedSchedule(schedule))
15
+ }
@@ -0,0 +1,27 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3
+ import { z } from 'zod'
4
+
5
+ const server = new McpServer({
6
+ name: 'swarmclaw-fake-mcp',
7
+ version: '1.0.0',
8
+ })
9
+
10
+ server.registerTool('ping', {
11
+ description: 'Returns pong for smoke tests',
12
+ inputSchema: {},
13
+ }, async () => ({
14
+ content: [{ type: 'text', text: 'pong' }],
15
+ }))
16
+
17
+ server.registerTool('echo', {
18
+ description: 'Echoes a caller-provided message',
19
+ inputSchema: {
20
+ message: z.string(),
21
+ },
22
+ }, async ({ message }) => ({
23
+ content: [{ type: 'text', text: `echo: ${message}` }],
24
+ }))
25
+
26
+ const transport = new StdioServerTransport()
27
+ await server.connect(transport)
@@ -0,0 +1,16 @@
1
+ import type { Agent } from '@/types'
2
+
3
+ export function isAgentDisabled(agent: Pick<Agent, 'disabled'> | null | undefined): boolean {
4
+ return agent?.disabled === true
5
+ }
6
+
7
+ export function buildAgentDisabledMessage(
8
+ agent: Pick<Agent, 'name'> | null | undefined,
9
+ action?: string,
10
+ ): string {
11
+ const name = typeof agent?.name === 'string' && agent.name.trim()
12
+ ? agent.name.trim()
13
+ : 'This agent'
14
+ if (action) return `${name} is disabled and cannot ${action}. Re-enable it to continue.`
15
+ return `${name} is disabled. Re-enable it to continue.`
16
+ }
@@ -6,6 +6,7 @@ import type {
6
6
  ProviderType,
7
7
  } from '@/types'
8
8
  import { deriveOpenClawWsUrl, normalizeProviderEndpoint } from '@/lib/openclaw-endpoint'
9
+ import { getProvider } from '@/lib/providers'
9
10
  import { loadGatewayProfiles } from './storage'
10
11
  import { isProviderCoolingDown } from './provider-health'
11
12
 
@@ -75,6 +76,8 @@ function normalizeGatewayDeployment(
75
76
  useCase: normalizeText(deployment.useCase) as DeploymentConfig['useCase'],
76
77
  exposure: normalizeText(deployment.exposure) as DeploymentConfig['exposure'],
77
78
  managedBy: normalizeText(deployment.managedBy) as DeploymentConfig['managedBy'],
79
+ localInstanceId: normalizeText(deployment.localInstanceId),
80
+ localPort: normalizeNullableNumber(deployment.localPort),
78
81
  targetHost: normalizeText(deployment.targetHost),
79
82
  sshHost: normalizeText(deployment.sshHost),
80
83
  sshUser: normalizeText(deployment.sshUser),
@@ -247,6 +250,12 @@ function dedupeCredentialIds(primary: string | null | undefined, candidates: str
247
250
  return result
248
251
  }
249
252
 
253
+ function resolveProviderDefaultEndpoint(provider: string): string | null {
254
+ const info = getProvider(provider)
255
+ if (!info?.defaultEndpoint) return null
256
+ return normalizeProviderEndpoint(provider, info.defaultEndpoint) || info.defaultEndpoint.replace(/\/+$/, '')
257
+ }
258
+
250
259
  function buildRouteFromSeed(
251
260
  seed: RouteSeed,
252
261
  gatewayProfiles: GatewayProfile[],
@@ -267,10 +276,9 @@ function buildRouteFromSeed(
267
276
  const gatewayProfileId = gatewayProfile?.id ?? seed.gatewayProfileId ?? agentGatewayProfileId ?? null
268
277
 
269
278
  const providerFromGateway = gatewayProfile?.provider === 'openclaw' ? 'openclaw' : provider
270
- const apiEndpoint = normalizeProviderEndpoint(
271
- providerFromGateway,
272
- seed.apiEndpoint ?? gatewayProfile?.endpoint ?? null,
273
- )
279
+ const explicitEndpoint = seed.apiEndpoint ?? gatewayProfile?.endpoint ?? null
280
+ const apiEndpoint = normalizeProviderEndpoint(providerFromGateway, explicitEndpoint)
281
+ ?? resolveProviderDefaultEndpoint(providerFromGateway)
274
282
  const model = (seed.model || '').trim() || (providerFromGateway === 'openclaw' ? DEFAULT_OPENCLAW_MODEL : '')
275
283
  if (!providerFromGateway || !model) return null
276
284
 
@@ -56,6 +56,9 @@ describe('ensureAgentThreadSession', () => {
56
56
  fallbackCredentialIds: [],
57
57
  heartbeatEnabled: true,
58
58
  heartbeatIntervalSec: 600,
59
+ memoryScopeMode: 'agent',
60
+ memoryTierPreference: 'blended',
61
+ projectId: 'proj-1',
59
62
  createdAt: now,
60
63
  updatedAt: now,
61
64
  plugins: ['memory', 'web_search'],
@@ -80,6 +83,54 @@ describe('ensureAgentThreadSession', () => {
80
83
  assert.equal(output.session.shortcutForAgentId, 'molly')
81
84
  assert.equal(output.session.agentId, 'molly')
82
85
  assert.equal(output.session.heartbeatEnabled, true)
86
+ assert.equal(output.session.memoryScopeMode, 'agent')
87
+ assert.equal(output.session.memoryTierPreference, 'blended')
88
+ assert.equal(output.session.projectId, 'proj-1')
83
89
  assert.deepEqual(output.session.plugins, ['memory', 'web_search'])
84
90
  })
91
+
92
+ it('does not create a new shortcut chat when the agent is disabled', () => {
93
+ const output = runWithTempDataDir(`
94
+ const storageMod = await import('./src/lib/server/storage.ts')
95
+ const storage = storageMod.default || storageMod['module.exports'] || storageMod
96
+ const helperMod = await import('./src/lib/server/agent-thread-session.ts')
97
+ const ensureAgentThreadSession = helperMod.ensureAgentThreadSession
98
+ || helperMod.default?.ensureAgentThreadSession
99
+ || helperMod['module.exports']?.ensureAgentThreadSession
100
+
101
+ const now = Date.now()
102
+ storage.saveAgents({
103
+ molly: {
104
+ id: 'molly',
105
+ name: 'Molly',
106
+ description: 'Temporarily disabled helper',
107
+ provider: 'openai',
108
+ model: 'gpt-test',
109
+ credentialId: null,
110
+ apiEndpoint: null,
111
+ fallbackCredentialIds: [],
112
+ disabled: true,
113
+ heartbeatEnabled: true,
114
+ heartbeatIntervalSec: 600,
115
+ createdAt: now,
116
+ updatedAt: now,
117
+ plugins: ['memory'],
118
+ },
119
+ })
120
+
121
+ const session = ensureAgentThreadSession('molly')
122
+ const agents = storage.loadAgents()
123
+ const sessions = storage.loadSessions()
124
+
125
+ console.log(JSON.stringify({
126
+ sessionId: session?.id || null,
127
+ threadSessionId: agents.molly?.threadSessionId || null,
128
+ sessionCount: Object.keys(sessions).length,
129
+ }))
130
+ `)
131
+
132
+ assert.equal(output.sessionId, null)
133
+ assert.equal(output.threadSessionId, null)
134
+ assert.equal(output.sessionCount, 0)
135
+ })
85
136
  })
@@ -1,6 +1,7 @@
1
1
  import { genId } from '@/lib/id'
2
2
  import type { Agent, Session } from '@/types'
3
3
  import { applyResolvedRoute, resolvePrimaryAgentRoute } from './agent-runtime-config'
4
+ import { isAgentDisabled } from './agent-availability'
4
5
  import { WORKSPACE_DIR } from './data-dir'
5
6
  import { loadAgents, loadSessions, saveAgents, saveSessions } from './storage'
6
7
 
@@ -44,6 +45,9 @@ function buildThreadSession(agent: Agent, sessionId: string, user: string, creat
44
45
  heartbeatEnabled: agent.heartbeatEnabled || false,
45
46
  heartbeatIntervalSec: agent.heartbeatIntervalSec || null,
46
47
  heartbeatTarget: existing?.heartbeatTarget || null,
48
+ memoryScopeMode: agent.memoryScopeMode || null,
49
+ memoryTierPreference: agent.memoryTierPreference || null,
50
+ projectId: agent.projectId || existing?.projectId || null,
47
51
  sessionResetMode: existing?.sessionResetMode || null,
48
52
  sessionIdleTimeoutSec: existing?.sessionIdleTimeoutSec || null,
49
53
  sessionMaxAgeSec: existing?.sessionMaxAgeSec || null,
@@ -95,6 +99,7 @@ export function ensureAgentThreadSession(agentId: string, user = 'default'): Ses
95
99
 
96
100
  const sessions = loadSessions()
97
101
  const now = Date.now()
102
+ const disabled = isAgentDisabled(agent)
98
103
 
99
104
  const existingId = typeof agent.threadSessionId === 'string' ? agent.threadSessionId : ''
100
105
  if (existingId && sessions[existingId]) {
@@ -119,6 +124,8 @@ export function ensureAgentThreadSession(agentId: string, user = 'default'): Ses
119
124
  return session
120
125
  }
121
126
 
127
+ if (disabled) return null
128
+
122
129
  const sessionId = `agent-chat-${agentId}-${genId()}`
123
130
  const session = buildThreadSession(agent, sessionId, user, now)
124
131
  sessions[sessionId] = session