@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
@@ -329,6 +329,7 @@ function normalizeImage(rawImage: unknown, legacyImagePath?: string | null): Mem
329
329
  function initDb() {
330
330
  const db = new Database(DB_PATH)
331
331
  db.pragma('journal_mode = WAL')
332
+ db.pragma('busy_timeout = 5000')
332
333
 
333
334
  db.exec(`
334
335
  CREATE TABLE IF NOT EXISTS memories (
@@ -354,10 +355,15 @@ function initDb() {
354
355
  'linkedMemoryIds TEXT',
355
356
  '"references" TEXT',
356
357
  'image TEXT',
358
+ 'pinned INTEGER DEFAULT 0',
359
+ 'sharedWith TEXT',
357
360
  ]) {
358
361
  try { db.exec(`ALTER TABLE memories ADD COLUMN ${col}`) } catch { /* already exists */ }
359
362
  }
360
363
 
364
+ // Partial index for fast pinned-memory lookups
365
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_pinned ON memories(agentId, updatedAt DESC) WHERE pinned = 1`)
366
+
361
367
  // FTS5 virtual table for full-text search
362
368
  db.exec(`
363
369
  CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
@@ -448,14 +454,14 @@ function initDb() {
448
454
  insert: db.prepare(`
449
455
  INSERT INTO memories (
450
456
  id, agentId, sessionId, category, title, content, metadata, embedding,
451
- "references", filePaths, image, imagePath, linkedMemoryIds, createdAt, updatedAt
457
+ "references", filePaths, image, imagePath, linkedMemoryIds, pinned, sharedWith, createdAt, updatedAt
452
458
  )
453
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
459
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
454
460
  `),
455
461
  update: db.prepare(`
456
462
  UPDATE memories
457
463
  SET agentId=?, sessionId=?, category=?, title=?, content=?, metadata=?, embedding=?,
458
- "references"=?, filePaths=?, image=?, imagePath=?, linkedMemoryIds=?, updatedAt=?
464
+ "references"=?, filePaths=?, image=?, imagePath=?, linkedMemoryIds=?, pinned=?, sharedWith=?, updatedAt=?
459
465
  WHERE id=?
460
466
  `),
461
467
  delete: db.prepare(`DELETE FROM memories WHERE id=?`),
@@ -467,6 +473,9 @@ function initDb() {
467
473
  },
468
474
  listAll: db.prepare(`SELECT * FROM memories ORDER BY updatedAt DESC LIMIT ?`),
469
475
  listByAgent: db.prepare(`SELECT * FROM memories WHERE agentId=? ORDER BY updatedAt DESC LIMIT ?`),
476
+ listByAgentOrShared: db.prepare(`SELECT * FROM memories WHERE agentId=? OR sharedWith LIKE ? ORDER BY updatedAt DESC LIMIT ?`),
477
+ listPinnedByAgent: db.prepare(`SELECT * FROM memories WHERE pinned = 1 AND agentId = ? ORDER BY updatedAt DESC LIMIT ?`),
478
+ listPinnedAll: db.prepare(`SELECT * FROM memories WHERE pinned = 1 ORDER BY updatedAt DESC LIMIT ?`),
470
479
  search: db.prepare(`
471
480
  SELECT m.* FROM memories m
472
481
  INNER JOIN memories_fts f ON m.rowid = f.rowid
@@ -479,6 +488,12 @@ function initDb() {
479
488
  WHERE memories_fts MATCH ? AND m.agentId = ?
480
489
  LIMIT ${MAX_FTS_RESULT_ROWS}
481
490
  `),
491
+ searchByAgentOrShared: db.prepare(`
492
+ SELECT m.* FROM memories m
493
+ INNER JOIN memories_fts f ON m.rowid = f.rowid
494
+ WHERE memories_fts MATCH ? AND (m.agentId = ? OR m.sharedWith LIKE ?)
495
+ LIMIT ${MAX_FTS_RESULT_ROWS}
496
+ `),
482
497
  // Remove a linked ID from all memories that reference it (cleanup on delete)
483
498
  findMemoriesLinkingTo: db.prepare(`SELECT * FROM memories WHERE linkedMemoryIds LIKE ?`),
484
499
  updateLinks: db.prepare(`UPDATE memories SET linkedMemoryIds = ?, updatedAt = ? WHERE id = ?`),
@@ -489,6 +504,7 @@ function initDb() {
489
504
  LIMIT 1
490
505
  `),
491
506
  allRowsByUpdated: db.prepare(`SELECT * FROM memories ORDER BY updatedAt DESC`),
507
+ countsByAgent: db.prepare(`SELECT COALESCE(agentId, '_global') AS agentKey, COUNT(*) AS cnt FROM memories GROUP BY agentKey`),
492
508
  exactDuplicateBySessionCategory: db.prepare(`
493
509
  SELECT * FROM memories
494
510
  WHERE sessionId = ? AND category = ? AND title = ? AND content = ?
@@ -517,6 +533,8 @@ function initDb() {
517
533
  image,
518
534
  imagePath: image?.path || undefined,
519
535
  linkedMemoryIds: linkedMemoryIds.length ? linkedMemoryIds : undefined,
536
+ pinned: row.pinned === 1,
537
+ sharedWith: parseJsonSafe<string[]>(row.sharedWith, []).length ? parseJsonSafe<string[]>(row.sharedWith, []) : undefined,
520
538
  createdAt: typeof row.createdAt === 'number' ? row.createdAt : Date.now(),
521
539
  updatedAt: typeof row.updatedAt === 'number' ? row.updatedAt : Date.now(),
522
540
  }
@@ -562,6 +580,8 @@ function initDb() {
562
580
  const duplicate = stmts.exactDuplicateBySessionCategory.get(sessionId, category, title, content) as Record<string, unknown> | undefined
563
581
  if (duplicate) return rowToEntry(duplicate)
564
582
  }
583
+ const pinned = data.pinned ? 1 : 0
584
+ const sharedWith = Array.isArray(data.sharedWith) && data.sharedWith.length ? JSON.stringify(data.sharedWith) : null
565
585
  stmts.insert.run(
566
586
  id, data.agentId || null, sessionId,
567
587
  category, title, content,
@@ -572,6 +592,8 @@ function initDb() {
572
592
  image ? JSON.stringify(image) : null,
573
593
  image?.path || null,
574
594
  linkedMemoryIds.length ? JSON.stringify(linkedMemoryIds) : null,
595
+ pinned,
596
+ sharedWith,
575
597
  now, now,
576
598
  )
577
599
  // Compute embedding in background (fire-and-forget)
@@ -617,6 +639,8 @@ function initDb() {
617
639
  const nextLinked = normalizeLinkedMemoryIds(merged.linkedMemoryIds, id)
618
640
  const prevLinked = normalizeLinkedMemoryIds(existingEntry.linkedMemoryIds, id)
619
641
  const now = Date.now()
642
+ const pinnedVal = merged.pinned ? 1 : 0
643
+ const sharedWithVal = Array.isArray(merged.sharedWith) && merged.sharedWith.length ? JSON.stringify(merged.sharedWith) : null
620
644
  stmts.update.run(
621
645
  merged.agentId || null, merged.sessionId || null,
622
646
  merged.category, merged.title, merged.content,
@@ -627,6 +651,8 @@ function initDb() {
627
651
  image ? JSON.stringify(image) : null,
628
652
  image?.path || null,
629
653
  nextLinked.length ? JSON.stringify(nextLinked) : null,
654
+ pinnedVal,
655
+ sharedWithVal,
630
656
  now, id,
631
657
  )
632
658
 
@@ -755,11 +781,11 @@ function initDb() {
755
781
  search(query: string, agentId?: string): MemoryEntry[] {
756
782
  if (shouldSkipSearchQuery(query)) return []
757
783
  const startedAt = Date.now()
758
- // FTS keyword search
784
+ // FTS keyword search (includes memories shared with this agent)
759
785
  const ftsQuery = buildFtsQuery(query)
760
786
  const ftsResults: MemoryEntry[] = ftsQuery
761
787
  ? (agentId
762
- ? stmts.searchByAgent.all(ftsQuery, agentId) as any[]
788
+ ? stmts.searchByAgentOrShared.all(ftsQuery, agentId, `%"${agentId}"%`) as any[]
763
789
  : stmts.search.all(ftsQuery) as any[]
764
790
  ).map(rowToEntry)
765
791
  : []
@@ -838,11 +864,26 @@ function initDb() {
838
864
  list(agentId?: string, limit = 200): MemoryEntry[] {
839
865
  const safeLimit = Math.max(1, Math.min(500, Math.trunc(limit)))
840
866
  const rows = agentId
841
- ? stmts.listByAgent.all(agentId, safeLimit) as any[]
867
+ ? stmts.listByAgentOrShared.all(agentId, `%"${agentId}"%`, safeLimit) as any[]
842
868
  : stmts.listAll.all(safeLimit) as any[]
843
869
  return rows.map(rowToEntry)
844
870
  },
845
871
 
872
+ listPinned(agentId?: string, limit = 20): MemoryEntry[] {
873
+ const safeLimit = Math.max(1, Math.min(100, Math.trunc(limit)))
874
+ const rows = agentId
875
+ ? stmts.listPinnedByAgent.all(agentId, safeLimit) as any[]
876
+ : stmts.listPinnedAll.all(safeLimit) as any[]
877
+ return rows.map(rowToEntry)
878
+ },
879
+
880
+ countsByAgent(): Record<string, number> {
881
+ const rows = stmts.countsByAgent.all() as { agentKey: string; cnt: number }[]
882
+ const result: Record<string, number> = {}
883
+ for (const row of rows) result[row.agentKey] = row.cnt
884
+ return result
885
+ },
886
+
846
887
  getByAgent(agentId: string, limit = 200): MemoryEntry[] {
847
888
  const safeLimit = Math.max(1, Math.min(500, Math.trunc(limit)))
848
889
  return (stmts.listByAgent.all(agentId, safeLimit) as any[]).map(rowToEntry)
@@ -1019,6 +1060,8 @@ export function addKnowledge(params: {
1019
1060
  title: string
1020
1061
  content: string
1021
1062
  tags?: string[]
1063
+ scope?: 'global' | 'agent'
1064
+ agentIds?: string[]
1022
1065
  createdByAgentId?: string | null
1023
1066
  createdBySessionId?: string | null
1024
1067
  }): MemoryEntry {
@@ -1031,6 +1074,8 @@ export function addKnowledge(params: {
1031
1074
  content: params.content,
1032
1075
  metadata: {
1033
1076
  tags: params.tags || [],
1077
+ scope: params.scope || 'global',
1078
+ agentIds: params.scope === 'agent' ? (params.agentIds || []) : [],
1034
1079
  createdByAgentId: params.createdByAgentId || null,
1035
1080
  createdBySessionId: params.createdBySessionId || null,
1036
1081
  },
@@ -77,6 +77,7 @@ export class OpenClawGateway {
77
77
  private _connected = false
78
78
  private reconnectTimer: ReturnType<typeof setTimeout> | null = null
79
79
  private reconnectDelay = 800
80
+ private consecutiveFailures = 0
80
81
  private shouldReconnect = false
81
82
  private wsUrl = ''
82
83
  private token: string | undefined
@@ -104,6 +105,7 @@ export class OpenClawGateway {
104
105
  this.ws = result.ws
105
106
  this._connected = true
106
107
  this.reconnectDelay = 800
108
+ this.consecutiveFailures = 0
107
109
  console.log('[openclaw-gateway] Connected to gateway')
108
110
 
109
111
  this.ws.on('message', (data) => {
@@ -149,12 +151,18 @@ export class OpenClawGateway {
149
151
 
150
152
  private scheduleReconnect() {
151
153
  if (this.reconnectTimer || !this.shouldReconnect) return
154
+ this.consecutiveFailures++
155
+ // After many failures, back off to 10 minutes to avoid hammering a down server
156
+ const maxDelay = this.consecutiveFailures >= 10 ? 600_000 : 15_000
152
157
  this.reconnectTimer = setTimeout(() => {
153
158
  this.reconnectTimer = null
154
159
  if (!this.shouldReconnect) return
155
160
  this.doConnect().catch(() => {})
156
161
  }, this.reconnectDelay)
157
- this.reconnectDelay = Math.min(this.reconnectDelay * 2, 15_000)
162
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, maxDelay)
163
+ if (this.consecutiveFailures % 5 === 0) {
164
+ console.log(`[openclaw-gateway] ${this.consecutiveFailures} consecutive failures, next retry in ${Math.round(this.reconnectDelay / 1000)}s`)
165
+ }
158
166
  }
159
167
 
160
168
  private rejectAllPending(reason: string) {
@@ -111,3 +111,128 @@ export function getProviderHealthSnapshot(): Record<string, ProviderHealthState
111
111
  return out
112
112
  }
113
113
 
114
+ // ---------------------------------------------------------------------------
115
+ // Lightweight provider ping functions (extracted from check-provider/route.ts)
116
+ // ---------------------------------------------------------------------------
117
+
118
+ const PING_TIMEOUT_MS = 8_000
119
+
120
+ async function parseErrorMessage(res: Response, fallback: string): Promise<string> {
121
+ const text = await res.text().catch(() => '')
122
+ if (!text) return fallback
123
+ try {
124
+ const parsed = JSON.parse(text)
125
+ if (typeof parsed?.error?.message === 'string' && parsed.error.message.trim()) return parsed.error.message.trim()
126
+ if (typeof parsed?.error === 'string' && parsed.error.trim()) return parsed.error.trim()
127
+ if (typeof parsed?.message === 'string' && parsed.message.trim()) return parsed.message.trim()
128
+ if (typeof parsed?.detail === 'string' && parsed.detail.trim()) return parsed.detail.trim()
129
+ } catch { /* non-JSON */ }
130
+ return text.slice(0, 300).trim() || fallback
131
+ }
132
+
133
+ export const OPENAI_COMPATIBLE_DEFAULTS: Record<string, { name: string; defaultEndpoint: string }> = {
134
+ openai: { name: 'OpenAI', defaultEndpoint: 'https://api.openai.com/v1' },
135
+ google: { name: 'Google Gemini', defaultEndpoint: 'https://generativelanguage.googleapis.com/v1beta/openai' },
136
+ deepseek: { name: 'DeepSeek', defaultEndpoint: 'https://api.deepseek.com/v1' },
137
+ groq: { name: 'Groq', defaultEndpoint: 'https://api.groq.com/openai/v1' },
138
+ together: { name: 'Together AI', defaultEndpoint: 'https://api.together.xyz/v1' },
139
+ mistral: { name: 'Mistral AI', defaultEndpoint: 'https://api.mistral.ai/v1' },
140
+ xai: { name: 'xAI (Grok)', defaultEndpoint: 'https://api.x.ai/v1' },
141
+ fireworks: { name: 'Fireworks AI', defaultEndpoint: 'https://api.fireworks.ai/inference/v1' },
142
+ }
143
+
144
+ export async function pingOpenAiCompatible(
145
+ apiKey: string,
146
+ endpoint: string,
147
+ ): Promise<{ ok: boolean; message: string }> {
148
+ const normalizedEndpoint = endpoint.replace(/\/+$/, '')
149
+ const res = await fetch(`${normalizedEndpoint}/models`, {
150
+ headers: { authorization: `Bearer ${apiKey}` },
151
+ signal: AbortSignal.timeout(PING_TIMEOUT_MS),
152
+ cache: 'no-store',
153
+ })
154
+ if (!res.ok) {
155
+ const detail = await parseErrorMessage(res, `Provider returned ${res.status}.`)
156
+ return { ok: false, message: detail }
157
+ }
158
+ return { ok: true, message: 'Connected.' }
159
+ }
160
+
161
+ export async function pingAnthropic(apiKey: string): Promise<{ ok: boolean; message: string }> {
162
+ const res = await fetch('https://api.anthropic.com/v1/models', {
163
+ headers: {
164
+ 'x-api-key': apiKey,
165
+ 'anthropic-version': '2023-06-01',
166
+ },
167
+ signal: AbortSignal.timeout(PING_TIMEOUT_MS),
168
+ cache: 'no-store',
169
+ })
170
+ if (!res.ok) {
171
+ const detail = await parseErrorMessage(res, `Anthropic returned ${res.status}.`)
172
+ return { ok: false, message: detail }
173
+ }
174
+ return { ok: true, message: 'Connected to Anthropic.' }
175
+ }
176
+
177
+ export async function pingOllama(endpoint: string): Promise<{ ok: boolean; message: string }> {
178
+ const normalizedEndpoint = (endpoint || 'http://localhost:11434').replace(/\/+$/, '')
179
+ const res = await fetch(`${normalizedEndpoint}/api/tags`, {
180
+ signal: AbortSignal.timeout(PING_TIMEOUT_MS),
181
+ cache: 'no-store',
182
+ })
183
+ if (!res.ok) {
184
+ const detail = await parseErrorMessage(res, `Ollama returned ${res.status}.`)
185
+ return { ok: false, message: detail }
186
+ }
187
+ return { ok: true, message: 'Connected to Ollama.' }
188
+ }
189
+
190
+ export async function pingOpenClaw(
191
+ apiKey: string | undefined,
192
+ endpoint: string,
193
+ ): Promise<{ ok: boolean; message: string }> {
194
+ const { wsConnect } = await import('@/lib/providers/openclaw')
195
+ let url = (endpoint || 'http://localhost:18789').replace(/\/+$/, '')
196
+ if (!/^(https?|wss?):\/\//i.test(url)) url = `http://${url}`
197
+ const wsUrl = url.replace(/^http:/i, 'ws:').replace(/^https:/i, 'wss:')
198
+ const result = await wsConnect(wsUrl, apiKey || undefined, true, PING_TIMEOUT_MS)
199
+ if (result.ws) try { result.ws.close() } catch { /* ignore */ }
200
+ return { ok: result.ok, message: result.ok ? 'Connected to OpenClaw.' : result.message }
201
+ }
202
+
203
+ /**
204
+ * Ping a provider to check reachability. Returns `{ ok, message }`.
205
+ * Skips CLI-based providers (claude-cli, codex-cli, opencode-cli) — returns ok.
206
+ */
207
+ export async function pingProvider(
208
+ provider: string,
209
+ apiKey: string | undefined,
210
+ endpoint: string | undefined,
211
+ ): Promise<{ ok: boolean; message: string }> {
212
+ const CLI_PROVIDERS = ['claude-cli', 'codex-cli', 'opencode-cli']
213
+ if (CLI_PROVIDERS.includes(provider)) return { ok: true, message: 'CLI provider — skipped.' }
214
+
215
+ try {
216
+ if (provider === 'anthropic') {
217
+ if (!apiKey) return { ok: false, message: 'No API key configured.' }
218
+ return await pingAnthropic(apiKey)
219
+ }
220
+ if (provider === 'ollama') {
221
+ return await pingOllama(endpoint || 'http://localhost:11434')
222
+ }
223
+ if (provider === 'openclaw') {
224
+ return await pingOpenClaw(apiKey, endpoint || 'http://localhost:18789')
225
+ }
226
+ // OpenAI-compatible providers (openai, google, deepseek, groq, together, mistral, xai, fireworks, custom)
227
+ const defaults = OPENAI_COMPATIBLE_DEFAULTS[provider]
228
+ const resolvedEndpoint = endpoint || defaults?.defaultEndpoint
229
+ if (!resolvedEndpoint) return { ok: false, message: `No endpoint for provider "${provider}".` }
230
+ if (!apiKey) return { ok: false, message: 'No API key configured.' }
231
+ return await pingOpenAiCompatible(apiKey, resolvedEndpoint)
232
+ } catch (err: unknown) {
233
+ const msg = err instanceof Error && err.name === 'TimeoutError'
234
+ ? 'Connection timed out.'
235
+ : (err instanceof Error ? err.message : String(err))
236
+ return { ok: false, message: msg }
237
+ }
238
+ }
@@ -12,7 +12,8 @@ import { getCheckpointSaver } from './langgraph-checkpoint'
12
12
  import { isProtectedMainSession } from './main-session'
13
13
  import type { Agent, BoardTask, Message } from '@/types'
14
14
 
15
- let processing = false
15
+ // HMR-safe: pin processing flag to globalThis so hot reloads don't reset it
16
+ const _queueState = ((globalThis as Record<string, unknown>).__swarmclaw_queue__ ??= { processing: false }) as { processing: boolean }
16
17
 
17
18
  interface SessionMessageLike {
18
19
  role?: string
@@ -467,8 +468,8 @@ function dequeueNextRunnableTask(queue: string[], tasks: Record<string, BoardTas
467
468
  }
468
469
 
469
470
  export async function processNext() {
470
- if (processing) return
471
- processing = true
471
+ if (_queueState.processing) return
472
+ _queueState.processing = true
472
473
 
473
474
  try {
474
475
  // Recover orphaned tasks: status is 'queued' but missing from the queue array
@@ -805,7 +806,7 @@ export async function processNext() {
805
806
  }
806
807
  }
807
808
  } finally {
808
- processing = false
809
+ _queueState.processing = false
809
810
  }
810
811
  }
811
812
 
@@ -4,6 +4,8 @@ import { enqueueTask } from './queue'
4
4
  import { CronExpressionParser } from 'cron-parser'
5
5
  import { pushMainLoopEventToMainSessions } from './main-agent-loop'
6
6
  import { getScheduleSignatureKey } from '@/lib/schedule-dedupe'
7
+ import { enqueueSystemEvent } from './system-events'
8
+ import { requestHeartbeatNow } from './heartbeat-wake'
7
9
 
8
10
  const TICK_INTERVAL = 60_000 // 60 seconds
9
11
  let intervalId: ReturnType<typeof setInterval> | null = null
@@ -192,5 +194,11 @@ async function tick() {
192
194
  type: 'schedule_fired',
193
195
  text: `Schedule fired: "${schedule.name}" (${schedule.id}) run #${schedule.runNumber} — task ${taskId}`,
194
196
  })
197
+
198
+ // Enqueue system event + heartbeat wake for the schedule's agent
199
+ if (schedule.createdInSessionId) {
200
+ enqueueSystemEvent(schedule.createdInSessionId, `Schedule triggered: ${schedule.name}`)
201
+ }
202
+ requestHeartbeatNow({ agentId: schedule.agentId, reason: 'schedule' })
195
203
  }
196
204
  }
@@ -37,6 +37,7 @@ interface QueueEntry {
37
37
  maxRuntimeMs?: number
38
38
  modelOverride?: string
39
39
  heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
40
+ replyToId?: string
40
41
  resolve: (value: ExecuteChatTurnResult) => void
41
42
  reject: (error: Error) => void
42
43
  promise: Promise<ExecuteChatTurnResult>
@@ -245,6 +246,7 @@ async function drainExecution(executionKey: string): Promise<void> {
245
246
  onEvent: (event) => emitToSubscribers(next, event),
246
247
  modelOverride: next.modelOverride,
247
248
  heartbeatConfig: next.heartbeatConfig,
249
+ replyToId: next.replyToId,
248
250
  })
249
251
 
250
252
  const failed = !!result.error
@@ -344,6 +346,7 @@ export interface EnqueueSessionRunInput {
344
346
  maxRuntimeMs?: number
345
347
  modelOverride?: string
346
348
  heartbeatConfig?: { ackMaxChars: number; showOk: boolean; showAlerts: boolean; target: string | null }
349
+ replyToId?: string
347
350
  }
348
351
 
349
352
  export interface EnqueueSessionRunResult {
@@ -454,6 +457,7 @@ export function enqueueSessionRun(input: EnqueueSessionRunInput): EnqueueSession
454
457
  maxRuntimeMs: effectiveMaxRuntimeMs > 0 ? effectiveMaxRuntimeMs : undefined,
455
458
  modelOverride: input.modelOverride,
456
459
  heartbeatConfig: input.heartbeatConfig,
460
+ replyToId: input.replyToId,
457
461
  resolve,
458
462
  reject,
459
463
  promise,
@@ -0,0 +1,136 @@
1
+ import { z } from 'zod'
2
+ import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
+ import { loadChatrooms, saveChatrooms, loadAgents } from '../storage'
4
+ import { genId } from '@/lib/id'
5
+ import { notify } from '../ws-hub'
6
+ import type { ToolBuildContext } from './context'
7
+ import type { Chatroom } from '@/types'
8
+
9
+ export function buildChatroomTools(bctx: ToolBuildContext): StructuredToolInterface[] {
10
+ const tools: StructuredToolInterface[] = []
11
+ const { hasTool } = bctx
12
+
13
+ if (hasTool('manage_chatrooms')) {
14
+ tools.push(
15
+ tool(
16
+ async ({ action, chatroomId, name, description, agentIds, agentId, message }) => {
17
+ try {
18
+ const chatrooms = loadChatrooms() as Record<string, Chatroom>
19
+
20
+ if (action === 'list_chatrooms') {
21
+ const list = Object.values(chatrooms).map((cr) => ({
22
+ id: cr.id,
23
+ name: cr.name,
24
+ description: cr.description,
25
+ memberCount: cr.agentIds.length,
26
+ messageCount: cr.messages.length,
27
+ }))
28
+ return JSON.stringify(list)
29
+ }
30
+
31
+ if (action === 'create_chatroom') {
32
+ const id = genId()
33
+ const agents = loadAgents()
34
+ const validAgentIds = (agentIds || []).filter((aid: string) => agents[aid])
35
+ const chatroom: Chatroom = {
36
+ id,
37
+ name: name || 'New Chatroom',
38
+ description: description || '',
39
+ agentIds: validAgentIds,
40
+ messages: [],
41
+ createdAt: Date.now(),
42
+ updatedAt: Date.now(),
43
+ }
44
+ chatrooms[id] = chatroom
45
+ saveChatrooms(chatrooms)
46
+ notify('chatrooms')
47
+ return JSON.stringify({ ok: true, chatroom: { id, name: chatroom.name, agentIds: validAgentIds } })
48
+ }
49
+
50
+ if (!chatroomId) return 'Error: chatroomId is required for this action.'
51
+ const chatroom = chatrooms[chatroomId]
52
+ if (!chatroom) return `Error: chatroom not found: ${chatroomId}`
53
+
54
+ if (action === 'add_agent') {
55
+ if (!agentId) return 'Error: agentId is required.'
56
+ const agents = loadAgents()
57
+ if (!agents[agentId]) return `Error: agent not found: ${agentId}`
58
+ if (!chatroom.agentIds.includes(agentId)) {
59
+ chatroom.agentIds.push(agentId)
60
+ chatroom.updatedAt = Date.now()
61
+ chatrooms[chatroomId] = chatroom
62
+ saveChatrooms(chatrooms)
63
+ notify('chatrooms')
64
+ notify(`chatroom:${chatroomId}`)
65
+ }
66
+ return JSON.stringify({ ok: true, agentIds: chatroom.agentIds })
67
+ }
68
+
69
+ if (action === 'remove_agent') {
70
+ if (!agentId) return 'Error: agentId is required.'
71
+ chatroom.agentIds = chatroom.agentIds.filter((id: string) => id !== agentId)
72
+ chatroom.updatedAt = Date.now()
73
+ chatrooms[chatroomId] = chatroom
74
+ saveChatrooms(chatrooms)
75
+ notify('chatrooms')
76
+ notify(`chatroom:${chatroomId}`)
77
+ return JSON.stringify({ ok: true, agentIds: chatroom.agentIds })
78
+ }
79
+
80
+ if (action === 'list_members') {
81
+ const agents = loadAgents()
82
+ const members = chatroom.agentIds.map((id: string) => {
83
+ const agent = agents[id]
84
+ return agent ? { id, name: agent.name, description: agent.description } : { id, name: 'Unknown' }
85
+ })
86
+ return JSON.stringify(members)
87
+ }
88
+
89
+ if (action === 'send_message') {
90
+ if (!message) return 'Error: message is required.'
91
+ const msgId = genId()
92
+ const senderName = bctx.ctx?.agentId
93
+ ? (loadAgents()[bctx.ctx.agentId]?.name || 'Agent')
94
+ : 'Agent'
95
+ chatroom.messages.push({
96
+ id: msgId,
97
+ senderId: bctx.ctx?.agentId || 'agent',
98
+ senderName,
99
+ role: 'assistant' as const,
100
+ text: message,
101
+ mentions: [],
102
+ reactions: [],
103
+ time: Date.now(),
104
+ })
105
+ chatroom.updatedAt = Date.now()
106
+ chatrooms[chatroomId] = chatroom
107
+ saveChatrooms(chatrooms)
108
+ notify(`chatroom:${chatroomId}`)
109
+ return JSON.stringify({ ok: true, messageId: msgId })
110
+ }
111
+
112
+ return `Error: unknown action "${action}". Valid actions: list_chatrooms, create_chatroom, add_agent, remove_agent, list_members, send_message`
113
+ } catch (err: unknown) {
114
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
115
+ }
116
+ },
117
+ {
118
+ name: 'manage_chatrooms',
119
+ description: 'Manage chatrooms for multi-agent collaboration. Actions: list_chatrooms, create_chatroom, add_agent, remove_agent, list_members, send_message.',
120
+ schema: z.object({
121
+ action: z.enum(['list_chatrooms', 'create_chatroom', 'add_agent', 'remove_agent', 'list_members', 'send_message'])
122
+ .describe('The action to perform'),
123
+ chatroomId: z.string().optional().describe('Chatroom ID (required for most actions except list/create)'),
124
+ name: z.string().optional().describe('Chatroom name (for create_chatroom)'),
125
+ description: z.string().optional().describe('Chatroom description (for create_chatroom)'),
126
+ agentIds: z.array(z.string()).optional().describe('Initial agent IDs (for create_chatroom)'),
127
+ agentId: z.string().optional().describe('Agent ID (for add_agent/remove_agent)'),
128
+ message: z.string().optional().describe('Message text (for send_message)'),
129
+ }),
130
+ },
131
+ ),
132
+ )
133
+ }
134
+
135
+ return tools
136
+ }
@@ -1,6 +1,9 @@
1
1
  import { z } from 'zod'
2
2
  import { tool, type StructuredToolInterface } from '@langchain/core/tools'
3
- import { loadSessions, saveSessions } from '../storage'
3
+ import { HumanMessage } from '@langchain/core/messages'
4
+ import { loadSessions, saveSessions, loadCredentials, decryptKey } from '../storage'
5
+ import { buildChatModel } from '../build-llm'
6
+ import { getProvider } from '@/lib/providers'
4
7
  import type { ToolBuildContext } from './context'
5
8
 
6
9
  export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterface[] {
@@ -19,8 +22,8 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
19
22
  const systemPromptTokens = 2000
20
23
  const status = getContextStatus(messages, systemPromptTokens, session.provider, session.model)
21
24
  return JSON.stringify(status)
22
- } catch (err: any) {
23
- return `Error: ${err.message || String(err)}`
25
+ } catch (err: unknown) {
26
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
24
27
  }
25
28
  },
26
29
  {
@@ -46,20 +49,33 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
46
49
  return JSON.stringify({ status: 'no_action', reason: 'Not enough messages to compact', messageCount: messages.length })
47
50
  }
48
51
 
49
- const generateSummary = async (text: string): Promise<string> => {
50
- const lines = text.split('\n\n').filter(Boolean)
51
- const keyLines: string[] = []
52
- for (const line of lines) {
53
- if (line.length > 20) {
54
- keyLines.push(line.slice(0, 200))
55
- }
56
- }
57
- let summary = ''
58
- for (const line of keyLines) {
59
- if (summary.length + line.length > 2000) break
60
- summary += line + '\n'
52
+ // Resolve API key for the session's provider
53
+ let apiKey: string | null = null
54
+ const providerInfo = getProvider(session.provider)
55
+ if ((providerInfo?.requiresApiKey || providerInfo?.optionalApiKey) && session.credentialId) {
56
+ try {
57
+ const creds = loadCredentials()
58
+ const cred = creds[session.credentialId]
59
+ if (cred) apiKey = decryptKey(cred.encryptedKey)
60
+ } catch { /* continue without key */ }
61
+ }
62
+
63
+ // Build LLM summarizer using the session's provider/model
64
+ const generateSummary = async (prompt: string): Promise<string> => {
65
+ const llm = buildChatModel({
66
+ provider: session.provider,
67
+ model: session.model,
68
+ apiKey,
69
+ apiEndpoint: session.apiEndpoint,
70
+ })
71
+ const response = await llm.invoke([new HumanMessage(prompt)])
72
+ if (typeof response.content === 'string') return response.content
73
+ if (Array.isArray(response.content)) {
74
+ return response.content
75
+ .map((b: Record<string, unknown>) => (typeof b.text === 'string' ? b.text : ''))
76
+ .join('')
61
77
  }
62
- return summary.trim() || 'Previous conversation context was pruned.'
78
+ return ''
63
79
  }
64
80
 
65
81
  const result = await summarizeAndCompact({
@@ -67,6 +83,8 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
67
83
  keepLastN: keep,
68
84
  agentId: ctx?.agentId || session.agentId || null,
69
85
  sessionId: ctx.sessionId,
86
+ provider: session.provider,
87
+ model: session.model,
70
88
  generateSummary,
71
89
  })
72
90
 
@@ -84,8 +102,8 @@ export function buildContextTools(bctx: ToolBuildContext): StructuredToolInterfa
84
102
  summaryAdded: result.summaryAdded,
85
103
  remainingMessages: result.messages.length,
86
104
  })
87
- } catch (err: any) {
88
- return `Error: ${err.message || String(err)}`
105
+ } catch (err: unknown) {
106
+ return `Error: ${err instanceof Error ? err.message : String(err)}`
89
107
  }
90
108
  },
91
109
  {
@@ -16,6 +16,7 @@ import { buildConnectorTools } from './connector'
16
16
  import { buildContextTools } from './context-mgmt'
17
17
  import { buildSandboxTools } from './sandbox'
18
18
  import { buildOpenClawNodeTools } from './openclaw-nodes'
19
+ import { buildChatroomTools } from './chatroom'
19
20
 
20
21
  export type { ToolContext, SessionToolsResult }
21
22
  export { sweepOrphanedBrowsers, cleanupSessionBrowser, getActiveBrowserCount, hasActiveBrowser }
@@ -97,6 +98,7 @@ export async function buildSessionTools(cwd: string, enabledTools: string[], ctx
97
98
  ...buildContextTools(bctx),
98
99
  ...buildSandboxTools(bctx),
99
100
  ...buildOpenClawNodeTools(bctx),
101
+ ...buildChatroomTools(bctx),
100
102
  )
101
103
 
102
104
  // ---------------------------------------------------------------------------