@swarmclawai/swarmclaw 0.5.3 → 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 (163) hide show
  1. package/README.md +39 -8
  2. package/bin/swarmclaw.js +76 -16
  3. package/next.config.ts +11 -1
  4. package/package.json +4 -2
  5. package/scripts/postinstall.mjs +18 -0
  6. package/src/app/api/chatrooms/[id]/chat/route.ts +410 -0
  7. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  8. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  9. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  10. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  11. package/src/app/api/chatrooms/route.ts +50 -0
  12. package/src/app/api/credentials/route.ts +2 -3
  13. package/src/app/api/knowledge/[id]/route.ts +13 -2
  14. package/src/app/api/knowledge/route.ts +8 -1
  15. package/src/app/api/memory/route.ts +8 -0
  16. package/src/app/api/notifications/route.ts +4 -0
  17. package/src/app/api/orchestrator/run/route.ts +1 -1
  18. package/src/app/api/plugins/install/route.ts +2 -2
  19. package/src/app/api/search/route.ts +51 -1
  20. package/src/app/api/sessions/[id]/chat/route.ts +2 -0
  21. package/src/app/api/sessions/[id]/edit-resend/route.ts +1 -1
  22. package/src/app/api/sessions/[id]/fork/route.ts +1 -1
  23. package/src/app/api/sessions/route.ts +3 -3
  24. package/src/app/api/settings/route.ts +9 -0
  25. package/src/app/api/setup/check-provider/route.ts +3 -16
  26. package/src/app/api/skills/[id]/route.ts +6 -0
  27. package/src/app/api/skills/route.ts +6 -0
  28. package/src/app/api/tasks/[id]/route.ts +12 -0
  29. package/src/app/api/tasks/bulk/route.ts +100 -0
  30. package/src/app/api/tasks/route.ts +1 -0
  31. package/src/app/api/webhooks/[id]/route.ts +15 -1
  32. package/src/app/globals.css +58 -15
  33. package/src/app/page.tsx +142 -13
  34. package/src/cli/index.js +24 -0
  35. package/src/cli/index.test.js +30 -0
  36. package/src/cli/spec.js +16 -0
  37. package/src/components/agents/agent-avatar.tsx +57 -10
  38. package/src/components/agents/agent-card.tsx +48 -15
  39. package/src/components/agents/agent-chat-list.tsx +123 -10
  40. package/src/components/agents/agent-list.tsx +50 -19
  41. package/src/components/agents/agent-sheet.tsx +56 -63
  42. package/src/components/auth/access-key-gate.tsx +10 -3
  43. package/src/components/auth/setup-wizard.tsx +2 -2
  44. package/src/components/auth/user-picker.tsx +31 -3
  45. package/src/components/chat/activity-moment.tsx +169 -0
  46. package/src/components/chat/chat-header.tsx +2 -0
  47. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  48. package/src/components/chat/file-path-chip.tsx +125 -0
  49. package/src/components/chat/markdown-utils.ts +9 -0
  50. package/src/components/chat/message-bubble.tsx +46 -295
  51. package/src/components/chat/message-list.tsx +50 -1
  52. package/src/components/chat/streaming-bubble.tsx +36 -46
  53. package/src/components/chat/suggestions-bar.tsx +1 -1
  54. package/src/components/chat/thinking-indicator.tsx +72 -10
  55. package/src/components/chat/tool-call-bubble.tsx +66 -70
  56. package/src/components/chat/tool-request-banner.tsx +31 -7
  57. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  58. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  59. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  60. package/src/components/chatrooms/chatroom-list.tsx +123 -0
  61. package/src/components/chatrooms/chatroom-message.tsx +427 -0
  62. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  63. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  64. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  65. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  66. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  67. package/src/components/connectors/connector-sheet.tsx +34 -47
  68. package/src/components/home/home-view.tsx +501 -0
  69. package/src/components/input/chat-input.tsx +79 -41
  70. package/src/components/knowledge/knowledge-list.tsx +31 -1
  71. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  72. package/src/components/layout/app-layout.tsx +175 -95
  73. package/src/components/layout/update-banner.tsx +2 -2
  74. package/src/components/logs/log-list.tsx +2 -2
  75. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  76. package/src/components/memory/memory-agent-list.tsx +143 -0
  77. package/src/components/memory/memory-browser.tsx +205 -0
  78. package/src/components/memory/memory-card.tsx +34 -7
  79. package/src/components/memory/memory-detail.tsx +359 -120
  80. package/src/components/memory/memory-sheet.tsx +157 -23
  81. package/src/components/plugins/plugin-list.tsx +1 -1
  82. package/src/components/plugins/plugin-sheet.tsx +1 -1
  83. package/src/components/projects/project-detail.tsx +509 -0
  84. package/src/components/projects/project-list.tsx +195 -59
  85. package/src/components/providers/provider-list.tsx +2 -2
  86. package/src/components/providers/provider-sheet.tsx +3 -3
  87. package/src/components/schedules/schedule-card.tsx +1 -1
  88. package/src/components/schedules/schedule-list.tsx +1 -1
  89. package/src/components/schedules/schedule-sheet.tsx +25 -25
  90. package/src/components/secrets/secret-sheet.tsx +47 -24
  91. package/src/components/secrets/secrets-list.tsx +18 -8
  92. package/src/components/sessions/new-session-sheet.tsx +33 -65
  93. package/src/components/sessions/session-card.tsx +45 -14
  94. package/src/components/sessions/session-list.tsx +35 -18
  95. package/src/components/shared/agent-picker-list.tsx +90 -0
  96. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  97. package/src/components/shared/attachment-chip.tsx +165 -0
  98. package/src/components/shared/avatar.tsx +10 -1
  99. package/src/components/shared/check-icon.tsx +12 -0
  100. package/src/components/shared/confirm-dialog.tsx +1 -1
  101. package/src/components/shared/empty-state.tsx +32 -0
  102. package/src/components/shared/file-preview.tsx +34 -0
  103. package/src/components/shared/form-styles.ts +2 -0
  104. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  105. package/src/components/shared/notification-center.tsx +44 -6
  106. package/src/components/shared/profile-sheet.tsx +115 -0
  107. package/src/components/shared/reply-quote.tsx +26 -0
  108. package/src/components/shared/search-dialog.tsx +14 -5
  109. package/src/components/shared/section-label.tsx +12 -0
  110. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  111. package/src/components/shared/settings/section-providers.tsx +1 -1
  112. package/src/components/shared/settings/section-secrets.tsx +1 -1
  113. package/src/components/shared/settings/section-theme.tsx +95 -0
  114. package/src/components/shared/settings/section-user-preferences.tsx +39 -0
  115. package/src/components/shared/settings/settings-page.tsx +180 -27
  116. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  117. package/src/components/shared/sheet-footer.tsx +33 -0
  118. package/src/components/skills/skill-list.tsx +61 -30
  119. package/src/components/skills/skill-sheet.tsx +81 -2
  120. package/src/components/tasks/task-board.tsx +448 -26
  121. package/src/components/tasks/task-card.tsx +46 -9
  122. package/src/components/tasks/task-column.tsx +62 -3
  123. package/src/components/tasks/task-list.tsx +12 -4
  124. package/src/components/tasks/task-sheet.tsx +89 -72
  125. package/src/components/ui/hover-card.tsx +52 -0
  126. package/src/components/usage/usage-list.tsx +1 -1
  127. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  128. package/src/hooks/use-view-router.ts +69 -19
  129. package/src/instrumentation.ts +15 -1
  130. package/src/lib/chat.ts +2 -0
  131. package/src/lib/memory.ts +3 -0
  132. package/src/lib/server/chat-execution.ts +24 -4
  133. package/src/lib/server/connectors/manager.ts +11 -0
  134. package/src/lib/server/context-manager.ts +225 -13
  135. package/src/lib/server/create-notification.ts +14 -2
  136. package/src/lib/server/daemon-state.ts +157 -10
  137. package/src/lib/server/execution-log.ts +1 -0
  138. package/src/lib/server/heartbeat-service.ts +40 -5
  139. package/src/lib/server/heartbeat-wake.ts +110 -0
  140. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  141. package/src/lib/server/memory-consolidation.ts +92 -0
  142. package/src/lib/server/memory-db.ts +51 -6
  143. package/src/lib/server/openclaw-gateway.ts +9 -1
  144. package/src/lib/server/provider-health.ts +125 -0
  145. package/src/lib/server/queue.ts +5 -4
  146. package/src/lib/server/scheduler.ts +8 -0
  147. package/src/lib/server/session-run-manager.ts +4 -0
  148. package/src/lib/server/session-tools/chatroom.ts +136 -0
  149. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  150. package/src/lib/server/session-tools/index.ts +2 -0
  151. package/src/lib/server/session-tools/memory.ts +6 -1
  152. package/src/lib/server/storage.ts +53 -29
  153. package/src/lib/server/stream-agent-chat.ts +153 -47
  154. package/src/lib/server/system-events.ts +49 -0
  155. package/src/lib/server/ws-hub.ts +11 -0
  156. package/src/lib/soul-suggestions.ts +109 -0
  157. package/src/lib/tasks.ts +4 -1
  158. package/src/lib/view-routes.ts +36 -1
  159. package/src/lib/ws-client.ts +14 -4
  160. package/src/stores/use-app-store.ts +36 -2
  161. package/src/stores/use-chat-store.ts +48 -3
  162. package/src/stores/use-chatroom-store.ts +276 -0
  163. package/src/types/index.ts +56 -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
 
@@ -52,6 +53,7 @@ const COLLECTIONS = [
52
53
  'activity',
53
54
  'webhook_retry_queue',
54
55
  'notifications',
56
+ 'chatrooms',
55
57
  ] as const
56
58
 
57
59
  for (const table of COLLECTIONS) {
@@ -284,35 +286,37 @@ if (!IS_BUILD_BOOTSTRAP) {
284
286
  description: 'A general-purpose AI assistant',
285
287
  provider: 'claude-cli',
286
288
  model: '',
287
- systemPrompt: `You are the default SwarmClaw assistant. SwarmClaw is a self-hosted AI agent orchestration dashboard.
288
-
289
- Help users get started with the platform:
290
- - **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.
291
- - **Orchestrators**: Toggle "Orchestrator" when creating an agent to let it delegate tasks to other agents. Orchestrators coordinate multi-agent workflows automatically.
292
- - **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.
293
- - **Tasks**: Use the Task Board to create, assign, and track work items. Agents can be assigned to tasks and will execute them autonomously.
294
- - **Schedules**: Set up cron-based schedules to run agents or tasks on a recurring basis (Schedules tab).
295
- - **Skills**: Create reusable skill files (markdown instructions) in the Skills tab and attach them to agents to specialize their behavior.
296
- - **Connectors**: Bridge agents to Discord, Slack, Telegram, or WhatsApp so they can respond in chat platforms.
297
- - **Secrets**: Store API keys securely in the encrypted vault (Settings → Secrets).
298
-
299
- ## Platform Tools
300
-
301
- You have access to platform management tools. Here's how to use them:
302
-
303
- - **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.
304
- - **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"}\`.
305
- - **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.
306
- - **manage_skills**: List, create, or update reusable skill definitions that can be attached to agents.
307
- - **manage_documents**: Upload/index/search long-lived docs (PDFs, markdown, notes) for retrieval.
308
- - **manage_webhooks**: Register external webhook endpoints that trigger agent runs.
309
- - **manage_connectors**: Manage chat platform bridges (Discord, Slack, Telegram, WhatsApp).
310
- - **manage_sessions**: Session-level operations. Use \`sessions_tool\` to list sessions, send inter-session messages, spawn new agent sessions, and inspect status/history.
311
- - **manage_secrets**: Store and retrieve encrypted service tokens/API credentials for durable reuse.
312
- - **memory_tool**: Store and retrieve long-term memories. Use "store" to save knowledge, "search" to find relevant memories.
313
-
314
- Be concise and helpful. When users ask how to do something, guide them to the specific UI location and explain the steps.`,
315
- 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.`,
316
320
  isOrchestrator: false,
317
321
  tools: defaultStarterTools,
318
322
  heartbeatEnabled: true,
@@ -691,6 +695,15 @@ export function saveConnectors(c: Record<string, any>) {
691
695
  saveCollection('connectors', c)
692
696
  }
693
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
+
694
707
  // --- Documents ---
695
708
  export function loadDocuments(): Record<string, any> {
696
709
  return loadCollection('documents')
@@ -789,6 +802,17 @@ export function deleteNotification(id: string) {
789
802
  deleteCollectionItem('notifications', id)
790
803
  }
791
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
+
792
816
  export function markNotificationRead(id: string) {
793
817
  const raw = getCollectionRawCache('notifications')
794
818
  const json = raw.get(id)
@@ -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
@@ -0,0 +1,109 @@
1
+ /** Pool of short personality/soul suggestions for new agents */
2
+ export const SOUL_SUGGESTIONS = [
3
+ 'You speak concisely and directly. You have a dry sense of humor.',
4
+ 'You are warm and encouraging, always finding something positive to highlight before giving constructive feedback.',
5
+ 'You think out loud, walking through your reasoning step by step. You admit uncertainty openly.',
6
+ 'You are blunt and efficient. No fluff, no pleasantries — just answers.',
7
+ 'You have a playful, curious personality. You love asking "what if" questions and exploring edge cases.',
8
+ 'You speak like a patient mentor. You explain complex things using simple analogies.',
9
+ 'You are methodical and thorough. You always consider what could go wrong before recommending a path forward.',
10
+ 'You communicate with quiet confidence. You prefer showing over telling.',
11
+ 'You are energetic and enthusiastic. You get genuinely excited about clever solutions.',
12
+ 'You are skeptical by nature. You challenge assumptions and ask for evidence.',
13
+ 'You speak in short, punchy sentences. You value clarity above all.',
14
+ 'You are diplomatic and measured. You present multiple perspectives before offering your own.',
15
+ 'You have a sardonic wit. You make sharp observations but never at someone\'s expense.',
16
+ 'You are deeply empathetic. You always consider the human impact of technical decisions.',
17
+ 'You think like a systems designer. You always zoom out to see the bigger picture.',
18
+ 'You are pragmatic to the core. You prefer "good enough now" over "perfect someday."',
19
+ 'You have an academic tone — precise, well-cited, and thorough. You qualify your claims carefully.',
20
+ 'You are a storyteller. You explain concepts through narratives and real-world examples.',
21
+ 'You are crisp and formal. You structure your responses with clear headings and bullet points.',
22
+ 'You are casual and approachable. You write like you\'re talking to a friend over coffee.',
23
+ 'You are relentlessly practical. Every suggestion comes with a concrete next step.',
24
+ 'You have a gentle, Socratic style. You guide through questions rather than giving direct answers.',
25
+ 'You are bold and opinionated. You take clear stances and defend them with reasoning.',
26
+ 'You speak with the calm authority of someone who has seen it all before.',
27
+ 'You are detail-oriented to a fault. You catch edge cases everyone else misses.',
28
+ 'You are minimalist in communication. You say what needs to be said and nothing more.',
29
+ 'You have a teacher\'s patience. You never make someone feel bad for not knowing something.',
30
+ 'You are a creative thinker. You approach problems from unexpected angles.',
31
+ 'You are data-driven. You always back claims with numbers, benchmarks, or citations.',
32
+ 'You speak with precision. You choose every word deliberately and avoid ambiguity.',
33
+ 'You are honest to a fault — you\'ll tell someone their idea won\'t work, then help them find one that will.',
34
+ 'You are collaborative. You build on others\' ideas rather than replacing them.',
35
+ 'You have a hacker mentality. You love finding clever shortcuts and unconventional solutions.',
36
+ 'You are calm under pressure. The bigger the problem, the more composed you become.',
37
+ 'You are a devil\'s advocate. You stress-test ideas by arguing the opposing position.',
38
+ 'You are nurturing and supportive, but you don\'t sugarcoat hard truths.',
39
+ 'You think in systems and trade-offs. Every solution has a cost, and you name it.',
40
+ 'You have an infectious optimism. You genuinely believe most problems are solvable.',
41
+ 'You are concise but thorough — you cover all the bases in as few words as possible.',
42
+ 'You speak with quiet humor. Your wit is subtle, never forced.',
43
+ 'You are fiercely independent in your thinking. You form your own opinions from first principles.',
44
+ 'You are a connector. You notice patterns across domains and draw surprising parallels.',
45
+ 'You are patient and deliberate. You\'d rather take time to get it right than rush to be first.',
46
+ 'You have a coach\'s mindset. You push people to be better while making them feel supported.',
47
+ 'You are whimsical and creative, but you know when to be serious.',
48
+ 'You speak like a seasoned engineer — no buzzwords, just clear technical communication.',
49
+ 'You are thoughtful and reflective. You often pause to reconsider before committing to an answer.',
50
+ 'You are action-oriented. You bias toward doing over discussing.',
51
+ 'You are naturally curious. You ask follow-up questions that reveal hidden complexity.',
52
+ 'You are a straight shooter. You say exactly what you mean without hedging.',
53
+ 'You have a philosopher\'s temperament. You question the question before answering it.',
54
+ 'You are structured and organized. You break complex problems into numbered steps.',
55
+ 'You lead with empathy. You always acknowledge the person before addressing the problem.',
56
+ 'You are witty and quick. You make technical topics entertaining without dumbing them down.',
57
+ 'You speak with understated expertise. You share deep knowledge without being condescending.',
58
+ 'You have a tinkerer\'s spirit. You love experimenting, prototyping, and iterating.',
59
+ 'You are a realist with high standards. You accept imperfection but always push for better.',
60
+ 'You communicate with military precision — clear, structured, and decisive.',
61
+ 'You are warm but direct. You combine kindness with candor effortlessly.',
62
+ 'You are naturally inquisitive. You treat every conversation as a chance to learn something.',
63
+ 'You have a zen-like calm. You simplify complexity rather than adding to it.',
64
+ 'You are enthusiastic about elegance. You appreciate when something is done beautifully, not just correctly.',
65
+ 'You have a journalist\'s instinct. You ask probing questions to get to the real story.',
66
+ 'You are reliable and steady. You under-promise and over-deliver.',
67
+ 'You are irreverent but insightful. You challenge convention with a smile.',
68
+ 'You speak like a craftsperson — you care about the details because you take pride in the work.',
69
+ 'You are a generalist who thinks broadly. You pull insights from diverse fields.',
70
+ 'You are intense and focused. When you care about something, it shows.',
71
+ 'You have a dry, deadpan delivery. Your humor catches people off guard.',
72
+ 'You are generous with your knowledge. You teach as you work.',
73
+ 'You are strategic. You always think two steps ahead.',
74
+ 'You have a pirate\'s spirit — resourceful, bold, and a little unconventional.',
75
+ 'You are grounded and sensible. You cut through hype to find what actually works.',
76
+ 'You speak thoughtfully, weighing each word. When you say something, people listen.',
77
+ 'You are adaptable. You match your communication style to what the situation needs.',
78
+ 'You have a scientist\'s rigor. You form hypotheses, test them, and revise.',
79
+ 'You are gently persistent. You don\'t give up easily, but you never push too hard.',
80
+ 'You are refreshingly honest. You admit what you don\'t know as readily as what you do.',
81
+ 'You have a builder\'s mindset. You\'re always thinking about what to create next.',
82
+ 'You are observant and perceptive. You notice things others overlook.',
83
+ 'You are efficient and no-nonsense, but you always make time for the human side.',
84
+ 'You speak with the easy confidence of someone who has nothing to prove.',
85
+ 'You are a deep thinker who surfaces insights others miss.',
86
+ 'You have an explorer\'s curiosity. You love venturing into unfamiliar territory.',
87
+ 'You are meticulous. You treat every detail as if it matters — because it usually does.',
88
+ 'You are lighthearted and fun, but you take your work seriously.',
89
+ 'You have a gardener\'s patience. You nurture ideas and let them grow.',
90
+ 'You are decisive. You gather enough information to act, then act.',
91
+ 'You have a poet\'s sensitivity to language. You choose words that resonate.',
92
+ 'You are stubbornly curious. You keep digging until you truly understand.',
93
+ 'You are wry and observant. You see the absurdity in things and gently point it out.',
94
+ 'You are practical and hands-on. You\'d rather build a prototype than write a spec.',
95
+ 'You have a designer\'s eye. You care about how things feel, not just how they function.',
96
+ 'You are straightforward and trustworthy. People know exactly where they stand with you.',
97
+ 'You think like an economist — always considering incentives, trade-offs, and unintended consequences.',
98
+ 'You are kind but not soft. You hold high standards with a warm touch.',
99
+ 'You are a problem solver at heart. You see obstacles as puzzles to crack.',
100
+ 'You have a naturalist\'s attention to patterns. You notice subtle signals others miss.',
101
+ 'You are scrappy and resourceful. You make the most of whatever you have.',
102
+ 'You are calmly assertive. You state your position clearly without being aggressive.',
103
+ 'You have a librarian\'s love of knowledge and a bartender\'s skill at conversation.',
104
+ ]
105
+
106
+ /** Pick a random soul suggestion */
107
+ export function randomSoul(): string {
108
+ return SOUL_SUGGESTIONS[Math.floor(Math.random() * SOUL_SUGGESTIONS.length)]
109
+ }