@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
@@ -0,0 +1,43 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {
4
+ inferAutomaticMemoryCategory,
5
+ normalizeMemoryCategory,
6
+ shouldAutoCaptureMemory,
7
+ shouldInjectMemoryContext,
8
+ } from './memory-policy'
9
+
10
+ test('normalizeMemoryCategory maps flat categories into hierarchical buckets', () => {
11
+ assert.equal(normalizeMemoryCategory('preference', 'User prefers terse replies'), 'identity/preferences')
12
+ assert.equal(normalizeMemoryCategory('decision', 'Ship the Docker path'), 'projects/decisions')
13
+ assert.equal(normalizeMemoryCategory('error', 'Root cause found'), 'execution/errors')
14
+ assert.equal(normalizeMemoryCategory('project', 'Repo setup'), 'projects/context')
15
+ })
16
+
17
+ test('shouldInjectMemoryContext skips low-signal greetings and acknowledgements', () => {
18
+ assert.equal(shouldInjectMemoryContext('thanks'), false)
19
+ assert.equal(shouldInjectMemoryContext('hello'), false)
20
+ assert.equal(shouldInjectMemoryContext('Remember this for later'), false)
21
+ assert.equal(shouldInjectMemoryContext('Compare the current deployment plan with what we decided yesterday'), true)
22
+ })
23
+
24
+ test('shouldAutoCaptureMemory filters noisy turns', () => {
25
+ assert.equal(shouldAutoCaptureMemory({ message: 'thanks', response: 'Happy to help with that.', source: 'chat' }), false)
26
+ assert.equal(shouldAutoCaptureMemory({ message: 'Please save this to memory', response: 'Stored memory "note".', source: 'chat' }), false)
27
+ assert.equal(shouldAutoCaptureMemory({
28
+ message: 'We decided to use the shared staging environment and keep the worker count at 2 for now.',
29
+ response: 'Decision captured: shared staging, worker count 2, and we will revisit after load testing next week.',
30
+ source: 'chat',
31
+ }), true)
32
+ })
33
+
34
+ test('inferAutomaticMemoryCategory picks a stable automatic bucket', () => {
35
+ assert.equal(
36
+ inferAutomaticMemoryCategory('The user prefers direct status updates.', 'I will keep future updates terse and direct.'),
37
+ 'identity/preferences',
38
+ )
39
+ assert.equal(
40
+ inferAutomaticMemoryCategory('We decided to ship the GitHub import first.', 'Decision locked for the next milestone.'),
41
+ 'projects/decisions',
42
+ )
43
+ })
@@ -0,0 +1,132 @@
1
+ import type { MemoryEntry } from '@/types'
2
+
3
+ const ACK_RE = /^(?:ok(?:ay)?|cool|nice|got it|makes sense|thanks|thank you|thx|roger|copy|sounds good|sgtm|yep|yup|y|nope?|nah|kk|done)[.! ]*$/i
4
+ const GREETING_RE = /^(?:hi|hello|hey|yo|morning|good morning|good afternoon|good evening)[.! ]*$/i
5
+ const MEMORY_META_RE = /\b(?:remember|memory|memorize|store this|save this|forget)\b/i
6
+ const LOW_SIGNAL_RESPONSE_RE = /^(?:HEARTBEAT_OK|NO_MESSAGE)\b/i
7
+
8
+ function normalizeWhitespace(value: string): string {
9
+ return value.replace(/\s+/g, ' ').trim()
10
+ }
11
+
12
+ function lower(value: string | null | undefined): string {
13
+ return normalizeWhitespace(value || '').toLowerCase()
14
+ }
15
+
16
+ export function shouldInjectMemoryContext(message: string): boolean {
17
+ const trimmed = normalizeWhitespace(message)
18
+ if (!trimmed) return false
19
+ if (trimmed.length < 16 && (ACK_RE.test(trimmed) || GREETING_RE.test(trimmed))) return false
20
+ if (trimmed.length < 24 && MEMORY_META_RE.test(trimmed)) return false
21
+ return true
22
+ }
23
+
24
+ export function shouldAutoCaptureMemoryTurn(message: string, response: string): boolean {
25
+ const normalizedMessage = normalizeWhitespace(message)
26
+ const normalizedResponse = normalizeWhitespace(response)
27
+ if (normalizedMessage.length < 20 || normalizedResponse.length < 40) return false
28
+ if (ACK_RE.test(normalizedMessage) || GREETING_RE.test(normalizedMessage)) return false
29
+ if (LOW_SIGNAL_RESPONSE_RE.test(normalizedResponse)) return false
30
+ if (MEMORY_META_RE.test(normalizedMessage) && normalizedMessage.length < 120) return false
31
+ if (/^(?:sorry|i can(?:not|'t)|unable to|i do not have|i don't have)\b/i.test(normalizedResponse)) return false
32
+ return true
33
+ }
34
+
35
+ export function shouldAutoCaptureMemory(
36
+ input: { message?: string | null; response?: string | null } | string,
37
+ response?: string,
38
+ ): boolean {
39
+ if (typeof input === 'string') {
40
+ return shouldAutoCaptureMemoryTurn(input, response || '')
41
+ }
42
+ return shouldAutoCaptureMemoryTurn(input.message || '', input.response || '')
43
+ }
44
+
45
+ export function normalizeMemoryCategory(
46
+ input: string | null | undefined,
47
+ title: string | null | undefined,
48
+ content: string | null | undefined,
49
+ ): string {
50
+ const explicit = lower(input)
51
+ const sample = `${lower(title)}\n${lower(content)}`
52
+
53
+ const mapExplicit = (value: string): string | null => {
54
+ if (!value || value === 'note' || value === 'notes') return null
55
+ if (['preference', 'preferences', 'likes', 'dislikes'].includes(value)) return 'identity/preferences'
56
+ if (['identity', 'profile', 'persona'].includes(value)) return 'identity/profile'
57
+ if (['relationship', 'relationships', 'people'].includes(value)) return 'identity/relationships'
58
+ if (['decision', 'decisions', 'choice'].includes(value)) return 'projects/decisions'
59
+ if (['learning', 'learnings', 'lesson', 'lessons'].includes(value)) return 'projects/learnings'
60
+ if (['project', 'projects', 'task', 'tasks'].includes(value)) return 'projects/context'
61
+ if (['error', 'errors', 'incident', 'incidents', 'failure', 'failures'].includes(value)) return 'execution/errors'
62
+ if (['breadcrumb', 'execution', 'run', 'runs'].includes(value)) return 'operations/execution'
63
+ if (['fact', 'facts', 'knowledge', 'reference'].includes(value)) return 'knowledge/facts'
64
+ if (['working', 'scratch', 'draft'].includes(value)) return 'working/scratch'
65
+ if (value.includes('/')) return value
66
+ return value
67
+ }
68
+
69
+ const explicitMapped = mapExplicit(explicit)
70
+ if (explicitMapped) return explicitMapped
71
+
72
+ if (/\b(?:prefer(?:s|ence)?|likes?|dislikes?|favorite|timezone|pronouns|call me)\b/.test(sample)) {
73
+ return 'identity/preferences'
74
+ }
75
+ if (/\b(?:wife|husband|partner|friend|manager|teammate|client|customer|relationship)\b/.test(sample)) {
76
+ return 'identity/relationships'
77
+ }
78
+ if (/\b(?:decided|decision|approved|picked|selected|going with|will use)\b/.test(sample)) {
79
+ return 'projects/decisions'
80
+ }
81
+ if (/\b(?:learned|lesson|fixed|solved|root cause|failure|bug|regression|postmortem)\b/.test(sample)) {
82
+ return 'projects/learnings'
83
+ }
84
+ if (/\b(?:error|incident|stack trace|exception|crash)\b/.test(sample)) {
85
+ return 'execution/errors'
86
+ }
87
+ if (/\b(?:project|repo|repository|ticket|task|milestone|deadline|roadmap)\b/.test(sample)) {
88
+ return 'projects/context'
89
+ }
90
+ if (/\b(?:config|credential|endpoint|workspace|path|env var|environment|docker|sandbox)\b/.test(sample)) {
91
+ return 'operations/environment'
92
+ }
93
+ if (/\b(?:fact|documentation|reference|api|schema)\b/.test(sample)) {
94
+ return 'knowledge/facts'
95
+ }
96
+ return explicit && explicit !== 'note' && explicit !== 'notes' ? explicit : 'knowledge/facts'
97
+ }
98
+
99
+ export function buildMemoryDoctorReport(entries: MemoryEntry[], agentId?: string | null): string {
100
+ const topLevelCounts = new Map<string, number>()
101
+ let pinned = 0
102
+ let linked = 0
103
+ let shared = 0
104
+
105
+ for (const entry of entries) {
106
+ const category = normalizeMemoryCategory(entry.category, entry.title, entry.content)
107
+ const topLevel = category.split('/')[0] || 'other'
108
+ topLevelCounts.set(topLevel, (topLevelCounts.get(topLevel) || 0) + 1)
109
+ if (entry.pinned) pinned += 1
110
+ if (entry.linkedMemoryIds?.length) linked += 1
111
+ if (entry.sharedWith?.length) shared += 1
112
+ }
113
+
114
+ const categories = [...topLevelCounts.entries()]
115
+ .sort((left, right) => right[1] - left[1])
116
+ .map(([name, count]) => `- ${name}: ${count}`)
117
+
118
+ return [
119
+ 'Memory Doctor',
120
+ `Agent scope: ${agentId || 'global/all'}`,
121
+ `Visible memories: ${entries.length}`,
122
+ `Pinned: ${pinned}`,
123
+ `Linked: ${linked}`,
124
+ `Shared: ${shared}`,
125
+ categories.length ? 'Top-level categories:' : 'Top-level categories: none',
126
+ ...(categories.length ? categories : []),
127
+ ].join('\n')
128
+ }
129
+
130
+ export function inferAutomaticMemoryCategory(message: string, response: string): string {
131
+ return normalizeMemoryCategory('note', message, response)
132
+ }
@@ -0,0 +1,60 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import type { MemoryEntry } from '@/types'
4
+
5
+ import { getMemoryTierForCategory, partitionMemoriesByTier, shouldHideFromDurableRecall } from './memory-tiers'
6
+
7
+ test('getMemoryTierForCategory treats hierarchical execution and archive categories correctly', () => {
8
+ assert.equal(getMemoryTierForCategory('operations/execution'), 'working')
9
+ assert.equal(getMemoryTierForCategory('operations/execution/tool-run'), 'working')
10
+ assert.equal(getMemoryTierForCategory('working/scratch'), 'working')
11
+ assert.equal(getMemoryTierForCategory('session_archive'), 'archive')
12
+ assert.equal(getMemoryTierForCategory('projects/decisions'), 'durable')
13
+ })
14
+
15
+ test('partitionMemoriesByTier keeps auto execution notes out of durable memory', () => {
16
+ const partitioned = partitionMemoriesByTier([
17
+ { category: 'projects/decisions', metadata: undefined },
18
+ { category: 'operations/execution', metadata: undefined },
19
+ { category: 'session_archive', metadata: { tier: 'archive' } },
20
+ ])
21
+
22
+ assert.equal(partitioned.durable.length, 1)
23
+ assert.equal(partitioned.working.length, 1)
24
+ assert.equal(partitioned.archive.length, 1)
25
+ })
26
+
27
+ test('shouldHideFromDurableRecall hides superseded and auto-consolidated entries', () => {
28
+ const autoConsolidatedTitle: Pick<MemoryEntry, 'title' | 'metadata'> = {
29
+ title: '[auto-consolidated] Project Kodiak note',
30
+ metadata: undefined,
31
+ }
32
+ const autoConsolidatedOrigin: Pick<MemoryEntry, 'title' | 'metadata'> = {
33
+ title: 'Project Kodiak',
34
+ metadata: { origin: 'auto-consolidated' },
35
+ }
36
+ const supersededEntry: Pick<MemoryEntry, 'title' | 'metadata'> = {
37
+ title: 'Project Kodiak',
38
+ metadata: { supersededBy: 'abc123' },
39
+ }
40
+ const canonicalEntry: Pick<MemoryEntry, 'title' | 'metadata'> = {
41
+ title: 'Canonical project fact',
42
+ metadata: {},
43
+ }
44
+
45
+ assert.equal(shouldHideFromDurableRecall({
46
+ ...autoConsolidatedTitle,
47
+ }), true)
48
+
49
+ assert.equal(shouldHideFromDurableRecall({
50
+ ...autoConsolidatedOrigin,
51
+ }), true)
52
+
53
+ assert.equal(shouldHideFromDurableRecall({
54
+ ...supersededEntry,
55
+ }), true)
56
+
57
+ assert.equal(shouldHideFromDurableRecall({
58
+ ...canonicalEntry,
59
+ }), false)
60
+ })
@@ -8,6 +8,10 @@ const ARCHIVE_CATEGORIES = new Set(['session_archive'])
8
8
  export function getMemoryTierForCategory(category: unknown): MemoryTier {
9
9
  const normalized = typeof category === 'string' ? category.trim().toLowerCase() : ''
10
10
  if (ARCHIVE_CATEGORIES.has(normalized)) return 'archive'
11
+ if (normalized.startsWith('session_archive/')) return 'archive'
12
+ if (normalized === 'operations/execution' || normalized.startsWith('operations/execution/')) return 'working'
13
+ if (normalized === 'working/scratch' || normalized.startsWith('working/')) return 'working'
14
+ if (normalized === 'execution' || normalized.startsWith('execution/')) return 'working'
11
15
  if (WORKING_CATEGORIES.has(normalized)) return 'working'
12
16
  return 'durable'
13
17
  }
@@ -38,3 +42,15 @@ export function partitionMemoriesByTier<T extends Pick<MemoryEntry, 'category' |
38
42
  export function isWorkingMemoryCategory(category: unknown): boolean {
39
43
  return getMemoryTierForCategory(category) === 'working'
40
44
  }
45
+
46
+ export function shouldHideFromDurableRecall(
47
+ entry: Pick<MemoryEntry, 'title' | 'metadata'>,
48
+ ): boolean {
49
+ const metadata = entry.metadata || {}
50
+ const origin = typeof metadata.origin === 'string' ? metadata.origin.trim().toLowerCase() : ''
51
+ if (origin === 'auto-consolidated') return true
52
+ if (typeof metadata.supersededBy === 'string' && metadata.supersededBy.trim()) return true
53
+ if (typeof metadata.supersededAt === 'number' && Number.isFinite(metadata.supersededAt)) return true
54
+ const title = typeof entry.title === 'string' ? entry.title.trim().toLowerCase() : ''
55
+ return title.startsWith('[auto-consolidated]')
56
+ }
@@ -0,0 +1,58 @@
1
+ import { isOllamaCloudModel, stripOllamaCloudModelSuffix } from '@/lib/ollama-model'
2
+ import { PROVIDER_DEFAULTS } from '@/lib/providers/provider-defaults'
3
+
4
+ const OLLAMA_CLOUD_KEY_ENV_VARS = ['OLLAMA_API_KEY', 'OLLAMA_CLOUD_API_KEY'] as const
5
+
6
+ function clean(value: string | null | undefined): string | null {
7
+ if (typeof value !== 'string') return null
8
+ const trimmed = value.trim()
9
+ return trimmed || null
10
+ }
11
+
12
+ function isOllamaCloudEndpoint(endpoint: string | null | undefined): boolean {
13
+ const normalized = clean(endpoint)
14
+ if (!normalized) return false
15
+ return /^https?:\/\/(?:www\.)?ollama\.com(?:\/|$)/i.test(normalized)
16
+ }
17
+
18
+ function hasExplicitEndpoint(endpoint: string | null | undefined): boolean {
19
+ return clean(endpoint) !== null
20
+ }
21
+
22
+ export function resolveOllamaCloudApiKey(explicitApiKey?: string | null): string | null {
23
+ const explicit = clean(explicitApiKey)
24
+ if (explicit && explicit !== 'ollama') return explicit
25
+ for (const envName of OLLAMA_CLOUD_KEY_ENV_VARS) {
26
+ const candidate = clean(process.env[envName])
27
+ if (candidate) return candidate
28
+ }
29
+ return null
30
+ }
31
+
32
+ export function resolveOllamaRuntimeConfig(input: {
33
+ model?: string | null
34
+ apiKey?: string | null
35
+ apiEndpoint?: string | null
36
+ }): {
37
+ model: string
38
+ useCloud: boolean
39
+ apiKey: string | null
40
+ endpoint: string
41
+ } {
42
+ const rawModel = clean(input.model) || ''
43
+ const explicitApiKey = clean(input.apiKey)
44
+ const explicitEndpoint = clean(input.apiEndpoint)
45
+ const cloudApiKey = resolveOllamaCloudApiKey(explicitApiKey)
46
+ const useCloud = isOllamaCloudEndpoint(explicitEndpoint)
47
+ || (!hasExplicitEndpoint(explicitEndpoint) && (
48
+ Boolean(explicitApiKey && explicitApiKey !== 'ollama')
49
+ || (isOllamaCloudModel(rawModel) && Boolean(cloudApiKey))
50
+ ))
51
+
52
+ return {
53
+ model: useCloud ? (stripOllamaCloudModelSuffix(rawModel) || rawModel) : rawModel,
54
+ useCloud,
55
+ apiKey: useCloud ? cloudApiKey : explicitApiKey,
56
+ endpoint: useCloud ? PROVIDER_DEFAULTS.ollamaCloud : (explicitEndpoint || PROVIDER_DEFAULTS.ollama),
57
+ }
58
+ }
@@ -1,10 +1,20 @@
1
1
  import assert from 'node:assert/strict'
2
- import { test } from 'node:test'
2
+ import { afterEach, test } from 'node:test'
3
3
  import {
4
4
  buildOpenClawDeployBundle,
5
+ getOpenClawLocalDeployCollectionStatus,
5
6
  getOpenClawLocalDeployStatus,
7
+ getOpenClawRemoteDeployCollectionStatus,
8
+ getOpenClawRemoteDeployStatus,
6
9
  } from './openclaw-deploy.ts'
7
10
 
11
+ const GLOBAL_KEY = '__swarmclaw_openclaw_deploy__' as const
12
+ const originalRuntimeState = (globalThis as typeof globalThis & { [GLOBAL_KEY]?: unknown })[GLOBAL_KEY]
13
+
14
+ afterEach(() => {
15
+ ;(globalThis as typeof globalThis & { [GLOBAL_KEY]?: unknown })[GLOBAL_KEY] = originalRuntimeState
16
+ })
17
+
8
18
  test('docker smart deploy bundle uses official image and provider-specific metadata', () => {
9
19
  const bundle = buildOpenClawDeployBundle({
10
20
  template: 'docker',
@@ -66,10 +76,108 @@ test('render bundle stays aligned with the official repo flow', () => {
66
76
 
67
77
  test('local deploy status exposes a sensible default endpoint before startup', () => {
68
78
  const status = getOpenClawLocalDeployStatus()
79
+ const collection = getOpenClawLocalDeployCollectionStatus()
69
80
 
81
+ assert.equal(status.id, 'local-default')
82
+ assert.equal(status.isPrimary, true)
70
83
  assert.equal(status.running, false)
71
84
  assert.equal(status.port, 18789)
72
85
  assert.equal(status.endpoint, 'http://127.0.0.1:18789/v1')
73
86
  assert.equal(status.wsUrl, 'ws://127.0.0.1:18789')
74
87
  assert.match(status.launchCommand, /npx openclaw gateway run/)
88
+ assert.equal(collection.primaryId, null)
89
+ assert.deepEqual(collection.items, [])
90
+ })
91
+
92
+ test('remote deploy status exposes a sensible default record before startup', () => {
93
+ const status = getOpenClawRemoteDeployStatus()
94
+ const collection = getOpenClawRemoteDeployCollectionStatus()
95
+
96
+ assert.equal(status.id, 'remote-default')
97
+ assert.equal(status.name, 'Remote OpenClaw')
98
+ assert.equal(status.isPrimary, true)
99
+ assert.equal(status.active, false)
100
+ assert.equal(status.status, 'idle')
101
+ assert.equal(status.target, null)
102
+ assert.equal(collection.primaryId, null)
103
+ assert.deepEqual(collection.items, [])
104
+ })
105
+
106
+ test('legacy singleton remote runtime state is migrated into the keyed remote collection', () => {
107
+ ;(globalThis as typeof globalThis & { [GLOBAL_KEY]?: unknown })[GLOBAL_KEY] = {
108
+ locals: {},
109
+ primaryLocalId: null,
110
+ remote: {
111
+ processId: null,
112
+ action: 'ssh-deploy',
113
+ target: 'gateway.example.com',
114
+ startedAt: 123,
115
+ lastError: null,
116
+ lastSummary: 'Deploying OpenClaw to gateway.example.com over SSH.',
117
+ lastCommandPreview: 'ssh root@gateway.example.com ...',
118
+ lastBackupPath: null,
119
+ },
120
+ }
121
+
122
+ const status = getOpenClawRemoteDeployStatus()
123
+ const collection = getOpenClawRemoteDeployCollectionStatus()
124
+
125
+ assert.equal(status.id, 'remote-default')
126
+ assert.equal(status.name, 'gateway.example.com')
127
+ assert.equal(status.target, 'gateway.example.com')
128
+ assert.equal(status.action, 'ssh-deploy')
129
+ assert.equal(status.lastSummary, 'Deploying OpenClaw to gateway.example.com over SSH.')
130
+ assert.equal(collection.primaryId, 'remote-default')
131
+ assert.equal(collection.items.length, 1)
132
+ assert.equal(collection.items[0]?.id, 'remote-default')
133
+ })
134
+
135
+ test('remote deploy collection preserves multiple remotes and targeted lookup', () => {
136
+ ;(globalThis as typeof globalThis & { [GLOBAL_KEY]?: unknown })[GLOBAL_KEY] = {
137
+ locals: {},
138
+ primaryLocalId: null,
139
+ remotes: {
140
+ 'remote-alpha': {
141
+ name: 'alpha',
142
+ processId: null,
143
+ action: 'restart',
144
+ target: 'alpha.example.com',
145
+ startedAt: null,
146
+ createdAt: 10,
147
+ updatedAt: 20,
148
+ lastError: null,
149
+ lastSummary: 'Restarting OpenClaw on alpha.example.com.',
150
+ lastCommandPreview: 'ssh root@alpha.example.com docker compose restart',
151
+ lastBackupPath: null,
152
+ },
153
+ 'remote-beta': {
154
+ name: 'beta',
155
+ processId: null,
156
+ action: 'upgrade',
157
+ target: 'beta.example.com',
158
+ startedAt: null,
159
+ createdAt: 30,
160
+ updatedAt: 40,
161
+ lastError: null,
162
+ lastSummary: 'Pulling openclaw:latest and recreating the OpenClaw stack on beta.example.com.',
163
+ lastCommandPreview: 'ssh root@beta.example.com docker compose up -d',
164
+ lastBackupPath: null,
165
+ },
166
+ },
167
+ primaryRemoteId: 'remote-alpha',
168
+ }
169
+
170
+ const collection = getOpenClawRemoteDeployCollectionStatus()
171
+ const primary = getOpenClawRemoteDeployStatus()
172
+ const beta = getOpenClawRemoteDeployStatus('remote-beta')
173
+
174
+ assert.equal(collection.primaryId, 'remote-alpha')
175
+ assert.equal(collection.items.length, 2)
176
+ assert.equal(collection.items[0]?.id, 'remote-beta')
177
+ assert.equal(collection.items[1]?.id, 'remote-alpha')
178
+ assert.equal(primary.id, 'remote-alpha')
179
+ assert.equal(primary.isPrimary, true)
180
+ assert.equal(beta.id, 'remote-beta')
181
+ assert.equal(beta.target, 'beta.example.com')
182
+ assert.equal(beta.isPrimary, false)
75
183
  })