@swarmclawai/swarmclaw 0.5.2 → 0.6.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 (173) hide show
  1. package/README.md +42 -7
  2. package/bin/swarmclaw.js +76 -16
  3. package/next.config.ts +11 -1
  4. package/package.json +4 -2
  5. package/public/screenshots/agents.png +0 -0
  6. package/public/screenshots/dashboard.png +0 -0
  7. package/public/screenshots/providers.png +0 -0
  8. package/public/screenshots/tasks.png +0 -0
  9. package/scripts/postinstall.mjs +18 -0
  10. package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
  11. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  12. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  13. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  14. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  15. package/src/app/api/chatrooms/route.ts +50 -0
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/knowledge/[id]/route.ts +13 -2
  18. package/src/app/api/knowledge/route.ts +8 -1
  19. package/src/app/api/memory/route.ts +8 -0
  20. package/src/app/api/notifications/[id]/route.ts +27 -0
  21. package/src/app/api/notifications/route.ts +68 -0
  22. package/src/app/api/orchestrator/run/route.ts +1 -1
  23. package/src/app/api/plugins/install/route.ts +2 -2
  24. package/src/app/api/search/route.ts +155 -0
  25. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  26. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  27. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  28. package/src/app/api/sessions/route.ts +3 -3
  29. package/src/app/api/settings/route.ts +9 -0
  30. package/src/app/api/setup/check-provider/route.ts +3 -16
  31. package/src/app/api/skills/[id]/route.ts +6 -0
  32. package/src/app/api/skills/route.ts +6 -0
  33. package/src/app/api/tasks/[id]/route.ts +20 -0
  34. package/src/app/api/tasks/bulk/route.ts +100 -0
  35. package/src/app/api/tasks/route.ts +1 -0
  36. package/src/app/api/usage/route.ts +45 -0
  37. package/src/app/api/webhooks/[id]/route.ts +15 -1
  38. package/src/app/globals.css +58 -15
  39. package/src/app/page.tsx +142 -13
  40. package/src/cli/index.js +42 -0
  41. package/src/cli/index.test.js +30 -0
  42. package/src/cli/spec.js +32 -0
  43. package/src/components/agents/agent-avatar.tsx +57 -10
  44. package/src/components/agents/agent-card.tsx +48 -15
  45. package/src/components/agents/agent-chat-list.tsx +123 -10
  46. package/src/components/agents/agent-list.tsx +50 -19
  47. package/src/components/agents/agent-sheet.tsx +56 -63
  48. package/src/components/auth/access-key-gate.tsx +10 -3
  49. package/src/components/auth/setup-wizard.tsx +2 -2
  50. package/src/components/auth/user-picker.tsx +31 -3
  51. package/src/components/chat/activity-moment.tsx +169 -0
  52. package/src/components/chat/chat-header.tsx +2 -0
  53. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  54. package/src/components/chat/file-path-chip.tsx +125 -0
  55. package/src/components/chat/markdown-utils.ts +9 -0
  56. package/src/components/chat/message-bubble.tsx +46 -295
  57. package/src/components/chat/message-list.tsx +50 -1
  58. package/src/components/chat/streaming-bubble.tsx +36 -46
  59. package/src/components/chat/suggestions-bar.tsx +1 -1
  60. package/src/components/chat/thinking-indicator.tsx +72 -10
  61. package/src/components/chat/tool-call-bubble.tsx +66 -70
  62. package/src/components/chat/tool-request-banner.tsx +31 -7
  63. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  64. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  65. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  66. package/src/components/chatrooms/chatroom-list.tsx +123 -0
  67. package/src/components/chatrooms/chatroom-message.tsx +427 -0
  68. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  69. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  70. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  71. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  72. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  73. package/src/components/connectors/connector-sheet.tsx +34 -47
  74. package/src/components/home/home-view.tsx +501 -0
  75. package/src/components/input/chat-input.tsx +79 -41
  76. package/src/components/knowledge/knowledge-list.tsx +31 -1
  77. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  78. package/src/components/layout/app-layout.tsx +209 -83
  79. package/src/components/layout/mobile-header.tsx +2 -0
  80. package/src/components/layout/update-banner.tsx +2 -2
  81. package/src/components/logs/log-list.tsx +2 -2
  82. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  83. package/src/components/memory/memory-agent-list.tsx +143 -0
  84. package/src/components/memory/memory-browser.tsx +205 -0
  85. package/src/components/memory/memory-card.tsx +34 -7
  86. package/src/components/memory/memory-detail.tsx +359 -120
  87. package/src/components/memory/memory-sheet.tsx +157 -23
  88. package/src/components/plugins/plugin-list.tsx +1 -1
  89. package/src/components/plugins/plugin-sheet.tsx +1 -1
  90. package/src/components/projects/project-detail.tsx +509 -0
  91. package/src/components/projects/project-list.tsx +195 -59
  92. package/src/components/providers/provider-list.tsx +2 -2
  93. package/src/components/providers/provider-sheet.tsx +3 -3
  94. package/src/components/schedules/schedule-card.tsx +3 -2
  95. package/src/components/schedules/schedule-list.tsx +1 -1
  96. package/src/components/schedules/schedule-sheet.tsx +25 -25
  97. package/src/components/secrets/secret-sheet.tsx +47 -24
  98. package/src/components/secrets/secrets-list.tsx +18 -8
  99. package/src/components/sessions/new-session-sheet.tsx +33 -65
  100. package/src/components/sessions/session-card.tsx +45 -14
  101. package/src/components/sessions/session-list.tsx +35 -18
  102. package/src/components/shared/agent-picker-list.tsx +90 -0
  103. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  104. package/src/components/shared/attachment-chip.tsx +165 -0
  105. package/src/components/shared/avatar.tsx +10 -1
  106. package/src/components/shared/check-icon.tsx +12 -0
  107. package/src/components/shared/confirm-dialog.tsx +1 -1
  108. package/src/components/shared/empty-state.tsx +32 -0
  109. package/src/components/shared/file-preview.tsx +34 -0
  110. package/src/components/shared/form-styles.ts +2 -0
  111. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  112. package/src/components/shared/notification-center.tsx +223 -0
  113. package/src/components/shared/profile-sheet.tsx +115 -0
  114. package/src/components/shared/reply-quote.tsx +26 -0
  115. package/src/components/shared/search-dialog.tsx +296 -0
  116. package/src/components/shared/section-label.tsx +12 -0
  117. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  118. package/src/components/shared/settings/section-providers.tsx +1 -1
  119. package/src/components/shared/settings/section-secrets.tsx +1 -1
  120. package/src/components/shared/settings/section-theme.tsx +95 -0
  121. package/src/components/shared/settings/section-user-preferences.tsx +39 -0
  122. package/src/components/shared/settings/settings-page.tsx +180 -27
  123. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  124. package/src/components/shared/sheet-footer.tsx +33 -0
  125. package/src/components/skills/skill-list.tsx +61 -30
  126. package/src/components/skills/skill-sheet.tsx +81 -2
  127. package/src/components/tasks/task-board.tsx +448 -26
  128. package/src/components/tasks/task-card.tsx +46 -9
  129. package/src/components/tasks/task-column.tsx +62 -3
  130. package/src/components/tasks/task-list.tsx +12 -4
  131. package/src/components/tasks/task-sheet.tsx +89 -72
  132. package/src/components/ui/hover-card.tsx +52 -0
  133. package/src/components/usage/metrics-dashboard.tsx +78 -0
  134. package/src/components/usage/usage-list.tsx +1 -1
  135. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  136. package/src/hooks/use-view-router.ts +69 -19
  137. package/src/instrumentation.ts +15 -1
  138. package/src/lib/chat.ts +2 -0
  139. package/src/lib/cron-human.ts +114 -0
  140. package/src/lib/memory.ts +3 -0
  141. package/src/lib/server/chat-execution.ts +24 -4
  142. package/src/lib/server/connectors/manager.ts +11 -0
  143. package/src/lib/server/context-manager.ts +225 -13
  144. package/src/lib/server/create-notification.ts +42 -0
  145. package/src/lib/server/daemon-state.ts +165 -10
  146. package/src/lib/server/execution-log.ts +1 -0
  147. package/src/lib/server/heartbeat-service.ts +40 -5
  148. package/src/lib/server/heartbeat-wake.ts +110 -0
  149. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  150. package/src/lib/server/memory-consolidation.ts +92 -0
  151. package/src/lib/server/memory-db.ts +51 -6
  152. package/src/lib/server/openclaw-gateway.ts +9 -1
  153. package/src/lib/server/provider-health.ts +125 -0
  154. package/src/lib/server/queue.ts +5 -4
  155. package/src/lib/server/scheduler.ts +8 -0
  156. package/src/lib/server/session-run-manager.ts +4 -0
  157. package/src/lib/server/session-tools/chatroom.ts +136 -0
  158. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  159. package/src/lib/server/session-tools/index.ts +2 -0
  160. package/src/lib/server/session-tools/memory.ts +6 -1
  161. package/src/lib/server/storage.ts +80 -29
  162. package/src/lib/server/stream-agent-chat.ts +153 -47
  163. package/src/lib/server/system-events.ts +49 -0
  164. package/src/lib/server/ws-hub.ts +11 -0
  165. package/src/lib/soul-suggestions.ts +109 -0
  166. package/src/lib/tasks.ts +4 -1
  167. package/src/lib/view-routes.ts +36 -1
  168. package/src/lib/ws-client.ts +14 -4
  169. package/src/proxy.ts +79 -2
  170. package/src/stores/use-app-store.ts +94 -3
  171. package/src/stores/use-chat-store.ts +48 -3
  172. package/src/stores/use-chatroom-store.ts +276 -0
  173. package/src/types/index.ts +69 -2
@@ -15,7 +15,8 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
15
15
 
16
16
  tools.push(
17
17
  tool(
18
- async ({ action, key, value, category, query, scope, filePaths, references, project, imagePath, linkedMemoryIds, depth, linkedLimit, targetIds, tags }) => {
18
+ async (input) => {
19
+ const { action, key, value, category, query, scope, filePaths, references, project, imagePath, linkedMemoryIds, depth, linkedLimit, targetIds, tags, pinned, sharedWith } = input as Record<string, any>
19
20
  try {
20
21
  const scopeMode = scope || 'auto'
21
22
  const currentAgentId = ctx?.agentId || null
@@ -98,6 +99,8 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
98
99
  image: storedImage,
99
100
  imagePath: storedImage?.path || undefined,
100
101
  linkedMemoryIds,
102
+ pinned: pinned === true,
103
+ sharedWith: Array.isArray(sharedWith) ? sharedWith : undefined,
101
104
  })
102
105
  const memoryScope = entry.agentId ? 'agent' : 'shared'
103
106
  let result = `Stored ${memoryScope} memory "${key}" (id: ${entry.id})`
@@ -220,6 +223,8 @@ export function buildMemoryTools(bctx: ToolBuildContext): StructuredToolInterfac
220
223
  linkedLimit: z.number().optional().describe('Max linked memories expanded during traversal. Respects configured server cap.'),
221
224
  targetIds: z.array(z.string()).optional().describe('Memory IDs to link/unlink (for link/unlink actions)'),
222
225
  tags: z.array(z.string()).optional().describe('Tags for categorizing knowledge entries'),
226
+ pinned: z.boolean().optional().describe('Mark memory as pinned (always preloaded in agent context). For store action.'),
227
+ sharedWith: z.array(z.string()).optional().describe('Agent IDs to share this memory with (for store action). They can read it in their context.'),
223
228
  }),
224
229
  },
225
230
  ),
@@ -20,6 +20,7 @@ const DB_PATH = IS_BUILD_BOOTSTRAP ? ':memory:' : path.join(DATA_DIR, 'swarmclaw
20
20
  const db = new Database(DB_PATH)
21
21
  if (!IS_BUILD_BOOTSTRAP) {
22
22
  db.pragma('journal_mode = WAL')
23
+ db.pragma('busy_timeout = 5000')
23
24
  }
24
25
  db.pragma('foreign_keys = ON')
25
26
 
@@ -51,6 +52,8 @@ const COLLECTIONS = [
51
52
  'projects',
52
53
  'activity',
53
54
  'webhook_retry_queue',
55
+ 'notifications',
56
+ 'chatrooms',
54
57
  ] as const
55
58
 
56
59
  for (const table of COLLECTIONS) {
@@ -283,35 +286,37 @@ if (!IS_BUILD_BOOTSTRAP) {
283
286
  description: 'A general-purpose AI assistant',
284
287
  provider: 'claude-cli',
285
288
  model: '',
286
- systemPrompt: `You are the default SwarmClaw assistant. SwarmClaw is a self-hosted AI agent orchestration dashboard.
287
-
288
- Help users get started with the platform:
289
- - **Agents**: Create specialized AI agents (Agents tab → "+"). Each agent has a provider, model, system prompt, and optional tools (shell, files, web search, browser). Use "Generate with AI" to scaffold agents from a description.
290
- - **Orchestrators**: Toggle "Orchestrator" when creating an agent to let it delegate tasks to other agents. Orchestrators coordinate multi-agent workflows automatically.
291
- - **Providers**: Configure LLM backends in Settings → Providers. Built-in providers: Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama (local or cloud), and OpenClaw. You can also add custom OpenAI-compatible endpoints.
292
- - **Tasks**: Use the Task Board to create, assign, and track work items. Agents can be assigned to tasks and will execute them autonomously.
293
- - **Schedules**: Set up cron-based schedules to run agents or tasks on a recurring basis (Schedules tab).
294
- - **Skills**: Create reusable skill files (markdown instructions) in the Skills tab and attach them to agents to specialize their behavior.
295
- - **Connectors**: Bridge agents to Discord, Slack, Telegram, or WhatsApp so they can respond in chat platforms.
296
- - **Secrets**: Store API keys securely in the encrypted vault (Settings → Secrets).
297
-
298
- ## Platform Tools
299
-
300
- You have access to platform management tools. Here's how to use them:
301
-
302
- - **manage_agents**: List, create, update, or delete agents. Use action "list" to see all agents, "create" with a JSON data payload to add new ones.
303
- - **manage_tasks**: Create and manage task board items. Set "agentId" to assign a task to an agent, "status" to track progress (backlog → queued → running → completed/failed). Use action "create" with data like \`{"title": "...", "description": "...", "agentId": "...", "status": "backlog"}\`.
304
- - **manage_schedules**: Create recurring or one-time scheduled jobs. Set "scheduleType" to "cron", "interval", or "once". Provide "taskPrompt" for what the agent should do and "agentId" for who runs it.
305
- - **manage_skills**: List, create, or update reusable skill definitions that can be attached to agents.
306
- - **manage_documents**: Upload/index/search long-lived docs (PDFs, markdown, notes) for retrieval.
307
- - **manage_webhooks**: Register external webhook endpoints that trigger agent runs.
308
- - **manage_connectors**: Manage chat platform bridges (Discord, Slack, Telegram, WhatsApp).
309
- - **manage_sessions**: Session-level operations. Use \`sessions_tool\` to list sessions, send inter-session messages, spawn new agent sessions, and inspect status/history.
310
- - **manage_secrets**: Store and retrieve encrypted service tokens/API credentials for durable reuse.
311
- - **memory_tool**: Store and retrieve long-term memories. Use "store" to save knowledge, "search" to find relevant memories.
312
-
313
- Be concise and helpful. When users ask how to do something, guide them to the specific UI location and explain the steps.`,
314
- soul: '',
289
+ systemPrompt: `You are the SwarmClaw assistant. SwarmClaw is a self-hosted AI agent orchestration dashboard.
290
+
291
+ ## Platform
292
+
293
+ - **Agents** Create specialized AI agents (Agents tab → "+") with a provider, model, system prompt, and tools. "Generate with AI" scaffolds agents from a description. Toggle "Orchestrator" to let an agent delegate work to others.
294
+ - **Providers** Configure LLM backends in Settings → Providers: Claude Code CLI, OpenAI Codex CLI, OpenCode CLI, Anthropic, OpenAI, Google Gemini, DeepSeek, Groq, Together AI, Mistral AI, xAI (Grok), Fireworks AI, Ollama, OpenClaw, or custom OpenAI-compatible endpoints.
295
+ - **Tasks** The Task Board tracks work items. Assign agents and they'll execute autonomously.
296
+ - **Schedules** Cron-based recurring jobs that run agents or tasks automatically.
297
+ - **Skills** Reusable markdown instruction files you attach to agents to specialize them.
298
+ - **Connectors** Bridge agents to Discord, Slack, Telegram, or WhatsApp.
299
+ - **Secrets** Encrypted vault for API keys (Settings → Secrets).
300
+
301
+ ## Tools
302
+
303
+ Use your platform management tools proactively:
304
+
305
+ - **manage_agents**: List, create, update, or delete agents.
306
+ - **manage_tasks**: Create and manage task board items. Set status (backlog → queued → running → completed/failed) and assign agents.
307
+ - **manage_schedules**: Create recurring or one-time scheduled jobs with cron expressions or intervals.
308
+ - **manage_skills**: Manage reusable skill definitions.
309
+ - **manage_documents**: Upload, index, and search long-lived documents.
310
+ - **manage_webhooks**: Register webhook endpoints that trigger agent runs.
311
+ - **manage_connectors**: Manage chat platform bridges.
312
+ - **manage_sessions**: List chats, send inter-chat messages, spawn new agent chats.
313
+ - **manage_secrets**: Store and retrieve encrypted credentials.
314
+ - **memory_tool**: Store and retrieve long-term knowledge.`,
315
+ soul: `You're a knowledgeable, friendly guide who's genuinely enthusiastic about helping people build agent workflows. You adapt your tone to match the conversation — casual when exploring, precise when debugging, encouraging when learning.
316
+
317
+ You have opinions about good agent design. You suggest creative approaches, warn about common pitfalls, and get excited when someone gets something cool working. You're not a manual — you're a collaborator.
318
+
319
+ Be concise but not curt. Warmth doesn't require verbosity. When someone asks "how do I...?", give them the direct steps. Offer to do things rather than just explaining — if someone wants an agent created, create it. Use your tools when actions speak louder than words. If you don't know something, say so honestly.`,
315
320
  isOrchestrator: false,
316
321
  tools: defaultStarterTools,
317
322
  heartbeatEnabled: true,
@@ -690,6 +695,15 @@ export function saveConnectors(c: Record<string, any>) {
690
695
  saveCollection('connectors', c)
691
696
  }
692
697
 
698
+ // --- Chatrooms ---
699
+ export function loadChatrooms(): Record<string, any> {
700
+ return loadCollection('chatrooms')
701
+ }
702
+
703
+ export function saveChatrooms(c: Record<string, any>) {
704
+ saveCollection('chatrooms', c)
705
+ }
706
+
693
707
  // --- Documents ---
694
708
  export function loadDocuments(): Record<string, any> {
695
709
  return loadCollection('documents')
@@ -775,6 +789,43 @@ export function deleteWebhookRetry(id: string) {
775
789
  deleteCollectionItem('webhook_retry_queue', id)
776
790
  }
777
791
 
792
+ // --- Notifications ---
793
+ export function loadNotifications(): Record<string, unknown> {
794
+ return loadCollection('notifications')
795
+ }
796
+
797
+ export function saveNotification(id: string, data: unknown) {
798
+ upsertCollectionItem('notifications', id, data)
799
+ }
800
+
801
+ export function deleteNotification(id: string) {
802
+ deleteCollectionItem('notifications', id)
803
+ }
804
+
805
+ export function hasUnreadNotificationWithKey(dedupKey: string): boolean {
806
+ const raw = getCollectionRawCache('notifications')
807
+ for (const json of raw.values()) {
808
+ try {
809
+ const n = JSON.parse(json) as Record<string, unknown>
810
+ if (n.dedupKey === dedupKey && n.read !== true) return true
811
+ } catch { /* skip malformed */ }
812
+ }
813
+ return false
814
+ }
815
+
816
+ export function markNotificationRead(id: string) {
817
+ const raw = getCollectionRawCache('notifications')
818
+ const json = raw.get(id)
819
+ if (!json) return
820
+ try {
821
+ const notification = JSON.parse(json) as Record<string, unknown>
822
+ notification.read = true
823
+ upsertCollectionItem('notifications', id, notification)
824
+ } catch {
825
+ // ignore malformed
826
+ }
827
+ }
828
+
778
829
  export function getSessionMessages(sessionId: string): Message[] {
779
830
  const stmt = db.prepare('SELECT data FROM sessions WHERE id = ?')
780
831
  const row = stmt.get(sessionId) as { data: string } | undefined
@@ -12,6 +12,20 @@ import { logExecution } from './execution-log'
12
12
  import type { Session, Message, UsageRecord } from '@/types'
13
13
  import { extractSuggestions } from './suggestions'
14
14
 
15
+ /** Extract a breadcrumb title from notable tool completions (task/schedule/agent creation). */
16
+ function extractBreadcrumbTitle(toolName: string, input: unknown, output: string | undefined): string | null {
17
+ if (!input || typeof input !== 'object') return null
18
+ const inp = input as Record<string, unknown>
19
+ const action = typeof inp.action === 'string' ? inp.action : ''
20
+ if (toolName === 'manage_tasks') {
21
+ if (action === 'create') return `Created task: ${inp.title || 'Untitled'}`
22
+ if (output && /status.*completed|completed.*successfully/i.test(output)) return `Completed task: ${inp.title || inp.taskId || 'unknown'}`
23
+ }
24
+ if (toolName === 'manage_schedules' && action === 'create') return `Created schedule: ${inp.name || 'Untitled'}`
25
+ if (toolName === 'manage_agents' && action === 'create') return `Created agent: ${inp.name || 'Untitled'}`
26
+ return null
27
+ }
28
+
15
29
  interface StreamAgentChatOpts {
16
30
  session: Session
17
31
  message: string
@@ -25,7 +39,7 @@ interface StreamAgentChatOpts {
25
39
  signal?: AbortSignal
26
40
  }
27
41
 
28
- function buildToolCapabilityLines(enabledTools: string[]): string[] {
42
+ function buildToolCapabilityLines(enabledTools: string[], opts?: { platformAssignScope?: 'self' | 'all' }): string[] {
29
43
  const lines: string[] = []
30
44
  if (enabledTools.includes('shell')) lines.push('- Shell execution is available (`execute_command`). Use it for running servers, installing deps, running scripts, git commands, build/test steps, and any single or chained shell commands. Supports background mode for long-running processes like dev servers.')
31
45
  if (enabledTools.includes('process')) lines.push('- Process control is available (`process_tool`) for long-running commands (poll/log/write/kill).')
@@ -52,9 +66,12 @@ function buildToolCapabilityLines(enabledTools: string[]): string[] {
52
66
  // Context tools are available to any session with tools (not just manage_sessions)
53
67
  if (enabledTools.length > 0) {
54
68
  lines.push('- Context management is available (`context_status`, `context_summarize`). Use `context_status` to check token usage and `context_summarize` to compact conversation history when approaching limits.')
55
- lines.push('- Agent delegation is available (`delegate_to_agent`). Use it to assign tasks to other agents based on their capabilities.')
69
+ if (opts?.platformAssignScope === 'all') {
70
+ lines.push('- Agent delegation is available (`delegate_to_agent`). Use it to assign tasks to other agents based on their capabilities.')
71
+ }
56
72
  }
57
73
  if (enabledTools.includes('manage_secrets')) lines.push('- Secret management is available (`manage_secrets`) for durable encrypted credentials and API tokens.')
74
+ if (enabledTools.includes('manage_chatrooms')) lines.push('- Chatroom management is available (`manage_chatrooms`) for multi-agent collaborative chatrooms with @mention-based interactions.')
58
75
  return lines
59
76
  }
60
77
 
@@ -63,9 +80,10 @@ function buildAgenticExecutionPolicy(opts: {
63
80
  loopMode: 'bounded' | 'ongoing'
64
81
  heartbeatPrompt: string
65
82
  heartbeatIntervalSec: number
83
+ platformAssignScope?: 'self' | 'all'
66
84
  }) {
67
85
  const hasTooling = opts.enabledTools.length > 0
68
- const toolLines = buildToolCapabilityLines(opts.enabledTools)
86
+ const toolLines = buildToolCapabilityLines(opts.enabledTools, { platformAssignScope: opts.platformAssignScope })
69
87
  const delegationOrder = [
70
88
  opts.enabledTools.includes('claude_code') ? '`delegate_to_claude_code`' : null,
71
89
  opts.enabledTools.includes('codex_cli') ? '`delegate_to_codex_cli`' : null,
@@ -260,57 +278,92 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
260
278
  .map((h) => h.text),
261
279
  ].join('\n')
262
280
 
263
- const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, session.agentId, 1, 10, 14)
264
- const relevant = relevantLookup.entries.slice(0, 6)
265
- const recent = memDb.list(session.agentId, 12).slice(0, 6)
266
-
267
281
  const seen = new Set<string>()
268
- const formatMemoryLine = (m: any) => {
282
+ const formatMemoryLine = (m: { category?: string; title?: string; content?: string; pinned?: boolean }) => {
269
283
  const category = String(m.category || 'note')
270
284
  const title = String(m.title || 'Untitled').replace(/\s+/g, ' ').trim()
271
285
  const snippet = String(m.content || '').replace(/\s+/g, ' ').trim().slice(0, 220)
272
- return `- [${category}] ${title}: ${snippet}`
286
+ const pin = m.pinned ? ' [pinned]' : ''
287
+ return `- [${category}]${pin} ${title}: ${snippet}`
273
288
  }
274
289
 
290
+ // Pinned memories always appear first
291
+ const pinned = memDb.listPinned(session.agentId, 5)
292
+ const pinnedLines = pinned
293
+ .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
294
+ .map(formatMemoryLine)
295
+
296
+ // Reduce relevant slice by pinned count to keep total context bounded
297
+ const relevantSlice = Math.max(2, 6 - pinnedLines.length)
298
+ const relevantLookup = memDb.searchWithLinked(memoryQuerySeed, session.agentId, 1, 10, 14)
299
+ const relevant = relevantLookup.entries.slice(0, relevantSlice)
300
+ const recent = memDb.list(session.agentId, 12).slice(0, 6)
301
+
275
302
  const relevantLines = relevant
276
- .filter((m) => {
277
- if (!m?.id || seen.has(m.id)) return false
278
- seen.add(m.id)
279
- return true
280
- })
303
+ .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
281
304
  .map(formatMemoryLine)
282
305
 
283
306
  const recentLines = recent
284
- .filter((m) => {
285
- if (!m?.id || seen.has(m.id)) return false
286
- seen.add(m.id)
287
- return true
288
- })
307
+ .filter((m) => { if (!m?.id || seen.has(m.id)) return false; seen.add(m.id); return true })
289
308
  .map(formatMemoryLine)
290
309
 
291
310
  const memorySections: string[] = []
311
+ if (pinnedLines.length) {
312
+ memorySections.push(
313
+ ['## Pinned Memories', 'Always-loaded memories marked as important.', ...pinnedLines].join('\n'),
314
+ )
315
+ }
292
316
  if (relevantLines.length) {
293
317
  memorySections.push(
294
- [
295
- '## Relevant Memory Hits',
296
- 'These memories were retrieved by relevance for the current objective.',
297
- ...relevantLines,
298
- ].join('\n'),
318
+ ['## Relevant Memory Hits', 'These memories were retrieved by relevance for the current objective.', ...relevantLines].join('\n'),
299
319
  )
300
320
  }
301
321
  if (recentLines.length) {
302
322
  memorySections.push(
303
- [
304
- '## Recent Memory Notes',
305
- 'Recent durable notes that may still apply.',
306
- ...recentLines,
307
- ].join('\n'),
323
+ ['## Recent Memory Notes', 'Recent durable notes that may still apply.', ...recentLines].join('\n'),
308
324
  )
309
325
  }
310
326
 
311
327
  if (memorySections.length) {
312
328
  stateModifierParts.push(memorySections.join('\n\n'))
313
329
  }
330
+
331
+ // Memory Policy — always injected when memory tool is available
332
+ stateModifierParts.push([
333
+ '## Memory Policy',
334
+ 'You have long-term memory. Use it proactively — do not wait to be asked.',
335
+ '',
336
+ '**Store memories for:**',
337
+ '- User preferences, corrections, or explicit "remember this" requests',
338
+ '- Key decisions or outcomes from complex tasks',
339
+ '- Discovered facts about projects, codebases, or environments',
340
+ '- Errors encountered and their solutions',
341
+ '- Relationship context (who is who, team dynamics)',
342
+ '- Important configuration details or environment specifics',
343
+ '',
344
+ '**Do NOT store:**',
345
+ '- Trivial acknowledgments or small talk',
346
+ '- Temporary in-progress work (use category "working" for ephemeral notes)',
347
+ '- Information already in your system prompt',
348
+ '- Exact duplicates of memories you already have',
349
+ '',
350
+ '**Best practices:**',
351
+ '- Use descriptive titles ("User prefers dark mode" not "Note 1")',
352
+ '- Use categories: preference, fact, learning, project, identity, decision',
353
+ '- Search memory before storing to avoid duplicates',
354
+ '- When correcting old knowledge, update or delete the old memory',
355
+ ].join('\n'))
356
+
357
+ // Pre-compaction memory flush: nudge agent to persist learnings when conversation is long
358
+ const msgCount = history.filter(m => m.role === 'user' || m.role === 'assistant').length
359
+ if (msgCount > 20) {
360
+ stateModifierParts.push([
361
+ '## Memory Flush Reminder',
362
+ 'This conversation is getting long. Before context is trimmed, store any important',
363
+ 'learnings, decisions, or facts as memories now. Only store what is significant and durable —',
364
+ 'skip trivial details. If nothing needs storing, continue normally.',
365
+ ].join('\n'))
366
+ }
314
367
  } catch {
315
368
  // If memory context fails to load, continue without blocking the run.
316
369
  }
@@ -363,6 +416,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
363
416
  loopMode: runtime.loopMode,
364
417
  heartbeatPrompt,
365
418
  heartbeatIntervalSec,
419
+ platformAssignScope: agentPlatformAssignScope,
366
420
  }),
367
421
  )
368
422
 
@@ -463,20 +517,35 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
463
517
  // Auto-compaction: prune old history if approaching context window limit
464
518
  let effectiveHistory = history
465
519
  try {
466
- const { shouldAutoCompact, consolidateToMemory, slidingWindowCompact, estimateTokens } = await import('./context-manager')
520
+ const { shouldAutoCompact, llmCompact, estimateTokens } = await import('./context-manager')
467
521
  const systemPromptTokens = estimateTokens(stateModifier)
468
522
  if (shouldAutoCompact(history, systemPromptTokens, session.provider, session.model)) {
469
- // Consolidate important old messages to memory before pruning
470
- const oldMessages = history.slice(0, -10)
471
- if (oldMessages.length > 0 && session.agentId) {
472
- consolidateToMemory(oldMessages, session.agentId, session.id)
523
+ const summarize = async (prompt: string): Promise<string> => {
524
+ const response = await llm.invoke([new HumanMessage(prompt)])
525
+ if (typeof response.content === 'string') return response.content
526
+ if (Array.isArray(response.content)) {
527
+ return response.content
528
+ .map((b: Record<string, unknown>) => (typeof b.text === 'string' ? b.text : ''))
529
+ .join('')
530
+ }
531
+ return ''
473
532
  }
474
- // Keep last 10 messages via sliding window
475
- effectiveHistory = slidingWindowCompact(history, 10)
476
- console.log(`[stream-agent-chat] Auto-compacted session ${session.id}: ${history.length} → ${effectiveHistory.length} messages`)
533
+ const result = await llmCompact({
534
+ messages: history,
535
+ provider: session.provider,
536
+ model: session.model,
537
+ agentId: session.agentId || null,
538
+ sessionId: session.id,
539
+ summarize,
540
+ })
541
+ effectiveHistory = result.messages
542
+ console.log(
543
+ `[stream-agent-chat] Auto-compacted ${session.id}: ${history.length} → ${effectiveHistory.length} msgs` +
544
+ (result.summaryAdded ? ' (LLM summary)' : ' (sliding window fallback)'),
545
+ )
477
546
  }
478
547
  } catch {
479
- // If context manager fails, continue with full history
548
+ // Context manager failure continue with full history
480
549
  }
481
550
 
482
551
  const langchainMessages: Array<HumanMessage | AIMessage> = []
@@ -497,6 +566,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
497
566
  let hasToolCalls = false
498
567
  let totalInputTokens = 0
499
568
  let totalOutputTokens = 0
569
+ let lastToolInput: unknown = null
500
570
 
501
571
  // Plugin hooks: beforeAgentStart
502
572
  const pluginMgr = getPluginManager()
@@ -529,15 +599,27 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
529
599
  const chunk = event.data?.chunk
530
600
  if (chunk?.content) {
531
601
  // content can be string or array of content blocks
532
- const text = typeof chunk.content === 'string'
533
- ? chunk.content
534
- : Array.isArray(chunk.content)
535
- ? chunk.content.map((c: any) => c.text || '').join('')
536
- : ''
537
- if (text) {
538
- fullText += text
539
- lastSegment += text
540
- write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
602
+ if (Array.isArray(chunk.content)) {
603
+ for (const block of chunk.content) {
604
+ // Anthropic extended thinking blocks
605
+ if (block.type === 'thinking' && block.thinking) {
606
+ write(`data: ${JSON.stringify({ t: 'thinking', text: block.thinking })}\n\n`)
607
+ // OpenClaw [[thinking]] prefix convention
608
+ } else if (typeof block.text === 'string' && block.text.startsWith('[[thinking]]')) {
609
+ write(`data: ${JSON.stringify({ t: 'thinking', text: block.text.slice(12) })}\n\n`)
610
+ } else if (block.text) {
611
+ fullText += block.text
612
+ lastSegment += block.text
613
+ write(`data: ${JSON.stringify({ t: 'd', text: block.text })}\n\n`)
614
+ }
615
+ }
616
+ } else {
617
+ const text = typeof chunk.content === 'string' ? chunk.content : ''
618
+ if (text) {
619
+ fullText += text
620
+ lastSegment += text
621
+ write(`data: ${JSON.stringify({ t: 'd', text })}\n\n`)
622
+ }
541
623
  }
542
624
  }
543
625
  } else if (kind === 'on_llm_end') {
@@ -554,6 +636,7 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
554
636
  lastSegment = ''
555
637
  const toolName = event.name || 'unknown'
556
638
  const input = event.data?.input
639
+ lastToolInput = input
557
640
  // Plugin hooks: beforeToolExec
558
641
  await pluginMgr.runHook('beforeToolExec', { toolName, input })
559
642
  const inputStr = typeof input === 'string' ? input : JSON.stringify(input)
@@ -576,6 +659,23 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
576
659
  : JSON.stringify(output)
577
660
  // Plugin hooks: afterToolExec
578
661
  await pluginMgr.runHook('afterToolExec', { toolName, input: null, output: outputStr })
662
+ // Event-driven memory breadcrumbs
663
+ if (session.agentId && (session.tools || []).includes('memory')) {
664
+ try {
665
+ const breadcrumbTitle = extractBreadcrumbTitle(toolName, lastToolInput, outputStr)
666
+ if (breadcrumbTitle) {
667
+ const memDb = getMemoryDb()
668
+ memDb.add({
669
+ agentId: session.agentId,
670
+ sessionId: session.id,
671
+ category: 'breadcrumb',
672
+ title: breadcrumbTitle,
673
+ content: '',
674
+ })
675
+ }
676
+ } catch { /* breadcrumbs are best-effort */ }
677
+ }
678
+ lastToolInput = null
579
679
  logExecution(session.id, 'tool_result', `${toolName} returned`, {
580
680
  agentId: session.agentId,
581
681
  detail: { toolName, output: outputStr?.slice(0, 4000), error: /^(Error:|error:)/i.test((outputStr || '').trim()) || undefined },
@@ -617,6 +717,12 @@ export async function streamAgentChat(opts: StreamAgentChatOpts): Promise<Stream
617
717
  if (signal) signal.removeEventListener('abort', abortFromSignal)
618
718
  }
619
719
 
720
+ // Skip post-stream work if the client disconnected mid-stream
721
+ if (signal?.aborted) {
722
+ await cleanup()
723
+ return { fullText, finalResponse: fullText }
724
+ }
725
+
620
726
  // Extract LLM-generated suggestions from the response and strip the tag
621
727
  const extracted = extractSuggestions(fullText)
622
728
  fullText = extracted.clean
@@ -0,0 +1,49 @@
1
+ /**
2
+ * In-memory event queue for heartbeat context injection.
3
+ * Events are accumulated between heartbeat ticks and drained into heartbeat prompts.
4
+ */
5
+
6
+ interface SystemEvent {
7
+ text: string
8
+ timestamp: number
9
+ contextKey?: string
10
+ }
11
+
12
+ const MAX_EVENTS_PER_SESSION = 20
13
+
14
+ const globalKey = '__swarmclaw_system_events__' as const
15
+ const globalScope = globalThis as typeof globalThis & { [globalKey]?: Map<string, SystemEvent[]> }
16
+ const queues: Map<string, SystemEvent[]> = globalScope[globalKey] ?? (globalScope[globalKey] = new Map())
17
+
18
+ /** Push an event for a session. Deduplicates consecutive identical text, caps at MAX_EVENTS_PER_SESSION. */
19
+ export function enqueueSystemEvent(sessionId: string, text: string, contextKey?: string): void {
20
+ let queue = queues.get(sessionId)
21
+ if (!queue) {
22
+ queue = []
23
+ queues.set(sessionId, queue)
24
+ }
25
+
26
+ // Deduplicate consecutive identical text
27
+ const last = queue[queue.length - 1]
28
+ if (last && last.text === text) return
29
+
30
+ queue.push({ text, timestamp: Date.now(), contextKey })
31
+
32
+ // Cap at max
33
+ if (queue.length > MAX_EVENTS_PER_SESSION) {
34
+ queue.splice(0, queue.length - MAX_EVENTS_PER_SESSION)
35
+ }
36
+ }
37
+
38
+ /** Destructive read — returns and clears all events for a session. */
39
+ export function drainSystemEvents(sessionId: string): SystemEvent[] {
40
+ const queue = queues.get(sessionId)
41
+ if (!queue || queue.length === 0) return []
42
+ queues.delete(sessionId)
43
+ return queue
44
+ }
45
+
46
+ /** Non-destructive read — returns current events without clearing. */
47
+ export function peekSystemEvents(sessionId: string): SystemEvent[] {
48
+ return queues.get(sessionId) || []
49
+ }
@@ -71,6 +71,17 @@ export function initWsServer() {
71
71
  console.log(`[ws-hub] WebSocket server listening on port ${port}`)
72
72
  }
73
73
 
74
+ export function closeWsServer(): Promise<void> {
75
+ const hub = getHub()
76
+ if (!hub) return Promise.resolve()
77
+ return new Promise((resolve) => {
78
+ for (const client of hub.clients) {
79
+ client.ws.close(1001, 'Server shutting down')
80
+ }
81
+ hub.wss.close(() => resolve())
82
+ })
83
+ }
84
+
74
85
  export function notify(topic: string, action = 'update', id?: string) {
75
86
  const hub = getHub()
76
87
  if (!hub) return