@swarmclawai/swarmclaw 0.5.3 → 0.6.2

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 (224) hide show
  1. package/README.md +53 -9
  2. package/bin/server-cmd.js +1 -0
  3. package/bin/swarmclaw.js +76 -16
  4. package/next.config.ts +11 -1
  5. package/package.json +5 -2
  6. package/scripts/postinstall.mjs +18 -0
  7. package/src/app/api/canvas/[sessionId]/route.ts +31 -0
  8. package/src/app/api/chatrooms/[id]/chat/route.ts +284 -0
  9. package/src/app/api/chatrooms/[id]/members/route.ts +82 -0
  10. package/src/app/api/chatrooms/[id]/pins/route.ts +39 -0
  11. package/src/app/api/chatrooms/[id]/reactions/route.ts +42 -0
  12. package/src/app/api/chatrooms/[id]/route.ts +84 -0
  13. package/src/app/api/chatrooms/route.ts +50 -0
  14. package/src/app/api/connectors/[id]/route.ts +1 -0
  15. package/src/app/api/connectors/route.ts +2 -1
  16. package/src/app/api/credentials/route.ts +2 -3
  17. package/src/app/api/files/open/route.ts +43 -0
  18. package/src/app/api/knowledge/[id]/route.ts +13 -2
  19. package/src/app/api/knowledge/route.ts +8 -1
  20. package/src/app/api/memory/route.ts +8 -0
  21. package/src/app/api/notifications/route.ts +4 -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 +53 -1
  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/[id]/messages/route.ts +70 -2
  29. package/src/app/api/sessions/[id]/route.ts +4 -0
  30. package/src/app/api/sessions/route.ts +3 -3
  31. package/src/app/api/settings/route.ts +9 -0
  32. package/src/app/api/setup/check-provider/route.ts +3 -16
  33. package/src/app/api/skills/[id]/route.ts +6 -0
  34. package/src/app/api/skills/route.ts +6 -0
  35. package/src/app/api/tasks/[id]/route.ts +12 -0
  36. package/src/app/api/tasks/bulk/route.ts +100 -0
  37. package/src/app/api/tasks/metrics/route.ts +101 -0
  38. package/src/app/api/tasks/route.ts +18 -2
  39. package/src/app/api/tts/route.ts +3 -2
  40. package/src/app/api/tts/stream/route.ts +3 -2
  41. package/src/app/api/uploads/[filename]/route.ts +19 -34
  42. package/src/app/api/uploads/route.ts +94 -0
  43. package/src/app/api/webhooks/[id]/route.ts +15 -1
  44. package/src/app/globals.css +63 -15
  45. package/src/app/page.tsx +142 -13
  46. package/src/cli/index.js +40 -1
  47. package/src/cli/index.test.js +30 -0
  48. package/src/cli/spec.js +42 -0
  49. package/src/components/agents/agent-avatar.tsx +57 -10
  50. package/src/components/agents/agent-card.tsx +50 -17
  51. package/src/components/agents/agent-chat-list.tsx +148 -12
  52. package/src/components/agents/agent-list.tsx +50 -19
  53. package/src/components/agents/agent-sheet.tsx +120 -65
  54. package/src/components/agents/inspector-panel.tsx +81 -6
  55. package/src/components/agents/openclaw-skills-panel.tsx +32 -3
  56. package/src/components/agents/personality-builder.tsx +42 -14
  57. package/src/components/agents/soul-library-picker.tsx +89 -0
  58. package/src/components/auth/access-key-gate.tsx +10 -3
  59. package/src/components/auth/setup-wizard.tsx +2 -2
  60. package/src/components/auth/user-picker.tsx +31 -3
  61. package/src/components/canvas/canvas-panel.tsx +96 -0
  62. package/src/components/chat/activity-moment.tsx +173 -0
  63. package/src/components/chat/chat-area.tsx +46 -22
  64. package/src/components/chat/chat-header.tsx +457 -286
  65. package/src/components/chat/chat-preview-panel.tsx +1 -2
  66. package/src/components/chat/chat-tool-toggles.tsx +1 -1
  67. package/src/components/chat/delegation-banner.tsx +371 -0
  68. package/src/components/chat/file-path-chip.tsx +146 -0
  69. package/src/components/chat/heartbeat-history-panel.tsx +269 -0
  70. package/src/components/chat/markdown-utils.ts +9 -0
  71. package/src/components/chat/message-bubble.tsx +356 -315
  72. package/src/components/chat/message-list.tsx +230 -8
  73. package/src/components/chat/streaming-bubble.tsx +104 -47
  74. package/src/components/chat/suggestions-bar.tsx +1 -1
  75. package/src/components/chat/thinking-indicator.tsx +72 -10
  76. package/src/components/chat/tool-call-bubble.tsx +111 -73
  77. package/src/components/chat/tool-request-banner.tsx +31 -7
  78. package/src/components/chat/transfer-agent-picker.tsx +63 -0
  79. package/src/components/chatrooms/agent-hover-card.tsx +124 -0
  80. package/src/components/chatrooms/chatroom-input.tsx +320 -0
  81. package/src/components/chatrooms/chatroom-list.tsx +130 -0
  82. package/src/components/chatrooms/chatroom-message.tsx +432 -0
  83. package/src/components/chatrooms/chatroom-sheet.tsx +215 -0
  84. package/src/components/chatrooms/chatroom-tool-request-banner.tsx +134 -0
  85. package/src/components/chatrooms/chatroom-typing-bar.tsx +88 -0
  86. package/src/components/chatrooms/chatroom-view.tsx +344 -0
  87. package/src/components/chatrooms/reaction-picker.tsx +273 -0
  88. package/src/components/connectors/connector-list.tsx +168 -90
  89. package/src/components/connectors/connector-sheet.tsx +95 -56
  90. package/src/components/home/home-view.tsx +501 -0
  91. package/src/components/input/chat-input.tsx +107 -43
  92. package/src/components/knowledge/knowledge-list.tsx +31 -1
  93. package/src/components/knowledge/knowledge-sheet.tsx +83 -2
  94. package/src/components/layout/app-layout.tsx +194 -97
  95. package/src/components/layout/update-banner.tsx +2 -2
  96. package/src/components/logs/log-list.tsx +2 -2
  97. package/src/components/mcp-servers/mcp-server-sheet.tsx +1 -1
  98. package/src/components/memory/memory-agent-list.tsx +143 -0
  99. package/src/components/memory/memory-browser.tsx +205 -0
  100. package/src/components/memory/memory-card.tsx +34 -7
  101. package/src/components/memory/memory-detail.tsx +359 -120
  102. package/src/components/memory/memory-sheet.tsx +157 -23
  103. package/src/components/plugins/plugin-list.tsx +1 -1
  104. package/src/components/plugins/plugin-sheet.tsx +1 -1
  105. package/src/components/projects/project-detail.tsx +509 -0
  106. package/src/components/projects/project-list.tsx +195 -59
  107. package/src/components/providers/provider-list.tsx +2 -2
  108. package/src/components/providers/provider-sheet.tsx +3 -3
  109. package/src/components/schedules/schedule-card.tsx +1 -1
  110. package/src/components/schedules/schedule-list.tsx +1 -1
  111. package/src/components/schedules/schedule-sheet.tsx +259 -126
  112. package/src/components/secrets/secret-sheet.tsx +47 -24
  113. package/src/components/secrets/secrets-list.tsx +18 -8
  114. package/src/components/sessions/new-session-sheet.tsx +33 -65
  115. package/src/components/sessions/session-card.tsx +45 -14
  116. package/src/components/sessions/session-list.tsx +35 -18
  117. package/src/components/settings/gateway-disconnect-overlay.tsx +80 -0
  118. package/src/components/shared/agent-picker-list.tsx +90 -0
  119. package/src/components/shared/agent-switch-dialog.tsx +156 -0
  120. package/src/components/shared/attachment-chip.tsx +165 -0
  121. package/src/components/shared/avatar.tsx +10 -1
  122. package/src/components/shared/chatroom-picker-list.tsx +61 -0
  123. package/src/components/shared/check-icon.tsx +12 -0
  124. package/src/components/shared/confirm-dialog.tsx +1 -1
  125. package/src/components/shared/connector-platform-icon.tsx +51 -4
  126. package/src/components/shared/empty-state.tsx +32 -0
  127. package/src/components/shared/file-preview.tsx +34 -0
  128. package/src/components/shared/form-styles.ts +2 -0
  129. package/src/components/shared/icon-button.tsx +16 -2
  130. package/src/components/shared/keyboard-shortcuts-dialog.tsx +116 -0
  131. package/src/components/shared/notification-center.tsx +44 -6
  132. package/src/components/shared/profile-sheet.tsx +115 -0
  133. package/src/components/shared/reply-quote.tsx +26 -0
  134. package/src/components/shared/search-dialog.tsx +31 -15
  135. package/src/components/shared/section-label.tsx +12 -0
  136. package/src/components/shared/settings/plugin-manager.tsx +1 -1
  137. package/src/components/shared/settings/section-embedding.tsx +48 -13
  138. package/src/components/shared/settings/section-orchestrator.tsx +46 -15
  139. package/src/components/shared/settings/section-providers.tsx +1 -1
  140. package/src/components/shared/settings/section-secrets.tsx +1 -1
  141. package/src/components/shared/settings/section-storage.tsx +206 -0
  142. package/src/components/shared/settings/section-theme.tsx +95 -0
  143. package/src/components/shared/settings/section-user-preferences.tsx +57 -0
  144. package/src/components/shared/settings/section-voice.tsx +42 -21
  145. package/src/components/shared/settings/section-web-search.tsx +30 -6
  146. package/src/components/shared/settings/settings-page.tsx +182 -27
  147. package/src/components/shared/settings/settings-sheet.tsx +9 -73
  148. package/src/components/shared/settings/storage-browser.tsx +259 -0
  149. package/src/components/shared/sheet-footer.tsx +33 -0
  150. package/src/components/skills/skill-list.tsx +61 -30
  151. package/src/components/skills/skill-sheet.tsx +81 -2
  152. package/src/components/tasks/task-board.tsx +448 -26
  153. package/src/components/tasks/task-card.tsx +59 -9
  154. package/src/components/tasks/task-column.tsx +62 -3
  155. package/src/components/tasks/task-list.tsx +12 -4
  156. package/src/components/tasks/task-sheet.tsx +416 -74
  157. package/src/components/ui/hover-card.tsx +52 -0
  158. package/src/components/usage/metrics-dashboard.tsx +90 -6
  159. package/src/components/usage/usage-list.tsx +1 -1
  160. package/src/components/webhooks/webhook-sheet.tsx +1 -1
  161. package/src/hooks/use-continuous-speech.ts +10 -4
  162. package/src/hooks/use-view-router.ts +69 -19
  163. package/src/hooks/use-voice-conversation.ts +53 -10
  164. package/src/hooks/use-ws.ts +4 -2
  165. package/src/instrumentation.ts +15 -1
  166. package/src/lib/chat.ts +2 -0
  167. package/src/lib/memory.ts +3 -0
  168. package/src/lib/providers/anthropic.ts +13 -7
  169. package/src/lib/providers/index.ts +1 -0
  170. package/src/lib/providers/openai.ts +13 -7
  171. package/src/lib/server/chat-execution.ts +75 -15
  172. package/src/lib/server/chatroom-helpers.ts +146 -0
  173. package/src/lib/server/connectors/manager.ts +229 -7
  174. package/src/lib/server/context-manager.ts +225 -13
  175. package/src/lib/server/create-notification.ts +14 -2
  176. package/src/lib/server/daemon-state.ts +157 -10
  177. package/src/lib/server/execution-log.ts +1 -0
  178. package/src/lib/server/heartbeat-service.ts +48 -6
  179. package/src/lib/server/heartbeat-wake.ts +110 -0
  180. package/src/lib/server/langgraph-checkpoint.ts +1 -0
  181. package/src/lib/server/main-agent-loop.ts +1 -1
  182. package/src/lib/server/memory-consolidation.ts +105 -0
  183. package/src/lib/server/memory-db.ts +183 -10
  184. package/src/lib/server/mime.ts +51 -0
  185. package/src/lib/server/openclaw-gateway.ts +9 -1
  186. package/src/lib/server/orchestrator-lg.ts +2 -0
  187. package/src/lib/server/orchestrator.ts +5 -2
  188. package/src/lib/server/playwright-proxy.mjs +2 -3
  189. package/src/lib/server/prompt-runtime-context.ts +53 -0
  190. package/src/lib/server/provider-health.ts +125 -0
  191. package/src/lib/server/queue.ts +56 -10
  192. package/src/lib/server/scheduler.ts +8 -0
  193. package/src/lib/server/session-run-manager.ts +4 -0
  194. package/src/lib/server/session-tools/canvas.ts +67 -0
  195. package/src/lib/server/session-tools/chatroom.ts +136 -0
  196. package/src/lib/server/session-tools/connector.ts +83 -9
  197. package/src/lib/server/session-tools/context-mgmt.ts +36 -18
  198. package/src/lib/server/session-tools/crud.ts +21 -0
  199. package/src/lib/server/session-tools/delegate.ts +68 -4
  200. package/src/lib/server/session-tools/git.ts +71 -0
  201. package/src/lib/server/session-tools/http.ts +57 -0
  202. package/src/lib/server/session-tools/index.ts +10 -0
  203. package/src/lib/server/session-tools/memory.ts +7 -1
  204. package/src/lib/server/session-tools/search-providers.ts +16 -8
  205. package/src/lib/server/session-tools/subagent.ts +106 -0
  206. package/src/lib/server/session-tools/web.ts +115 -4
  207. package/src/lib/server/storage.ts +53 -29
  208. package/src/lib/server/stream-agent-chat.ts +185 -57
  209. package/src/lib/server/system-events.ts +49 -0
  210. package/src/lib/server/task-mention.ts +41 -0
  211. package/src/lib/server/ws-hub.ts +11 -0
  212. package/src/lib/sessions.ts +10 -0
  213. package/src/lib/soul-library.ts +103 -0
  214. package/src/lib/soul-suggestions.ts +109 -0
  215. package/src/lib/task-dedupe.ts +26 -0
  216. package/src/lib/tasks.ts +4 -1
  217. package/src/lib/tool-definitions.ts +2 -0
  218. package/src/lib/tts.ts +2 -2
  219. package/src/lib/view-routes.ts +36 -1
  220. package/src/lib/ws-client.ts +14 -4
  221. package/src/stores/use-app-store.ts +41 -3
  222. package/src/stores/use-chat-store.ts +113 -5
  223. package/src/stores/use-chatroom-store.ts +276 -0
  224. package/src/types/index.ts +88 -4
@@ -1,6 +1,7 @@
1
1
  import Database from 'better-sqlite3'
2
2
  import path from 'path'
3
3
  import fs from 'fs'
4
+ import { createHash } from 'crypto'
4
5
  import { genId } from '@/lib/id'
5
6
  import type { MemoryEntry, FileReference, MemoryImage, MemoryReference } from '@/types'
6
7
  import { getEmbedding, cosineSimilarity, serializeEmbedding, deserializeEmbedding } from './embeddings'
@@ -32,6 +33,11 @@ export const MEMORY_FTS_STOP_WORDS = new Set([
32
33
  'you', 'your',
33
34
  ])
34
35
 
36
+ function computeContentHash(category: string, content: string): string {
37
+ const normalized = `${category}|${content.toLowerCase().trim()}`
38
+ return createHash('sha256').update(normalized).digest('hex').slice(0, 16)
39
+ }
40
+
35
41
  function shouldSkipSearchQuery(input: string): boolean {
36
42
  const text = String(input || '').toLowerCase().trim()
37
43
  if (!text) return true
@@ -329,6 +335,7 @@ function normalizeImage(rawImage: unknown, legacyImagePath?: string | null): Mem
329
335
  function initDb() {
330
336
  const db = new Database(DB_PATH)
331
337
  db.pragma('journal_mode = WAL')
338
+ db.pragma('busy_timeout = 5000')
332
339
 
333
340
  db.exec(`
334
341
  CREATE TABLE IF NOT EXISTS memories (
@@ -354,10 +361,22 @@ function initDb() {
354
361
  'linkedMemoryIds TEXT',
355
362
  '"references" TEXT',
356
363
  'image TEXT',
364
+ 'pinned INTEGER DEFAULT 0',
365
+ 'sharedWith TEXT',
366
+ 'accessCount INTEGER DEFAULT 0',
367
+ 'lastAccessedAt INTEGER DEFAULT 0',
368
+ 'contentHash TEXT',
369
+ 'reinforcementCount INTEGER DEFAULT 0',
357
370
  ]) {
358
371
  try { db.exec(`ALTER TABLE memories ADD COLUMN ${col}`) } catch { /* already exists */ }
359
372
  }
360
373
 
374
+ // Partial index for fast pinned-memory lookups
375
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_pinned ON memories(agentId, updatedAt DESC) WHERE pinned = 1`)
376
+
377
+ // Index for content hash dedup lookups
378
+ db.exec(`CREATE INDEX IF NOT EXISTS idx_memories_content_hash ON memories(contentHash) WHERE contentHash IS NOT NULL`)
379
+
361
380
  // FTS5 virtual table for full-text search
362
381
  db.exec(`
363
382
  CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
@@ -441,6 +460,24 @@ function initDb() {
441
460
  })
442
461
  migrateLegacyRows()
443
462
 
463
+ // Backfill contentHash for existing rows that don't have one yet
464
+ const unhashed = (db.prepare(`SELECT COUNT(*) as cnt FROM memories WHERE contentHash IS NULL`).get() as { cnt: number }).cnt
465
+ if (unhashed > 0) {
466
+ const backfillRows = db.prepare(`SELECT id, category, content FROM memories WHERE contentHash IS NULL`).all() as Array<{ id: string; category: string; content: string }>
467
+ const backfillStmt = db.prepare(`UPDATE memories SET contentHash = ? WHERE id = ?`)
468
+ const BATCH = 500
469
+ for (let i = 0; i < backfillRows.length; i += BATCH) {
470
+ const batch = backfillRows.slice(i, i + BATCH)
471
+ const tx = db.transaction(() => {
472
+ for (const r of batch) {
473
+ backfillStmt.run(computeContentHash(r.category, r.content), r.id)
474
+ }
475
+ })
476
+ tx()
477
+ }
478
+ console.log(`[memory-db] Backfilled contentHash for ${backfillRows.length} memory row(s)`)
479
+ }
480
+
444
481
  // Fresh installs now start with an empty memory graph.
445
482
  // Durable memories are created only from actual user/agent interactions.
446
483
 
@@ -448,14 +485,14 @@ function initDb() {
448
485
  insert: db.prepare(`
449
486
  INSERT INTO memories (
450
487
  id, agentId, sessionId, category, title, content, metadata, embedding,
451
- "references", filePaths, image, imagePath, linkedMemoryIds, createdAt, updatedAt
488
+ "references", filePaths, image, imagePath, linkedMemoryIds, pinned, sharedWith, contentHash, createdAt, updatedAt
452
489
  )
453
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
490
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
454
491
  `),
455
492
  update: db.prepare(`
456
493
  UPDATE memories
457
494
  SET agentId=?, sessionId=?, category=?, title=?, content=?, metadata=?, embedding=?,
458
- "references"=?, filePaths=?, image=?, imagePath=?, linkedMemoryIds=?, updatedAt=?
495
+ "references"=?, filePaths=?, image=?, imagePath=?, linkedMemoryIds=?, pinned=?, sharedWith=?, updatedAt=?
459
496
  WHERE id=?
460
497
  `),
461
498
  delete: db.prepare(`DELETE FROM memories WHERE id=?`),
@@ -467,6 +504,9 @@ function initDb() {
467
504
  },
468
505
  listAll: db.prepare(`SELECT * FROM memories ORDER BY updatedAt DESC LIMIT ?`),
469
506
  listByAgent: db.prepare(`SELECT * FROM memories WHERE agentId=? ORDER BY updatedAt DESC LIMIT ?`),
507
+ listByAgentOrShared: db.prepare(`SELECT * FROM memories WHERE agentId=? OR sharedWith LIKE ? ORDER BY updatedAt DESC LIMIT ?`),
508
+ listPinnedByAgent: db.prepare(`SELECT * FROM memories WHERE pinned = 1 AND agentId = ? ORDER BY updatedAt DESC LIMIT ?`),
509
+ listPinnedAll: db.prepare(`SELECT * FROM memories WHERE pinned = 1 ORDER BY updatedAt DESC LIMIT ?`),
470
510
  search: db.prepare(`
471
511
  SELECT m.* FROM memories m
472
512
  INNER JOIN memories_fts f ON m.rowid = f.rowid
@@ -479,6 +519,12 @@ function initDb() {
479
519
  WHERE memories_fts MATCH ? AND m.agentId = ?
480
520
  LIMIT ${MAX_FTS_RESULT_ROWS}
481
521
  `),
522
+ searchByAgentOrShared: db.prepare(`
523
+ SELECT m.* FROM memories m
524
+ INNER JOIN memories_fts f ON m.rowid = f.rowid
525
+ WHERE memories_fts MATCH ? AND (m.agentId = ? OR m.sharedWith LIKE ?)
526
+ LIMIT ${MAX_FTS_RESULT_ROWS}
527
+ `),
482
528
  // Remove a linked ID from all memories that reference it (cleanup on delete)
483
529
  findMemoriesLinkingTo: db.prepare(`SELECT * FROM memories WHERE linkedMemoryIds LIKE ?`),
484
530
  updateLinks: db.prepare(`UPDATE memories SET linkedMemoryIds = ?, updatedAt = ? WHERE id = ?`),
@@ -489,12 +535,31 @@ function initDb() {
489
535
  LIMIT 1
490
536
  `),
491
537
  allRowsByUpdated: db.prepare(`SELECT * FROM memories ORDER BY updatedAt DESC`),
538
+ countsByAgent: db.prepare(`SELECT COALESCE(agentId, '_global') AS agentKey, COUNT(*) AS cnt FROM memories GROUP BY agentKey`),
492
539
  exactDuplicateBySessionCategory: db.prepare(`
493
540
  SELECT * FROM memories
494
541
  WHERE sessionId = ? AND category = ? AND title = ? AND content = ?
495
542
  ORDER BY updatedAt DESC
496
543
  LIMIT 1
497
544
  `),
545
+ findByContentHash: db.prepare(`
546
+ SELECT * FROM memories
547
+ WHERE contentHash = ? AND agentId = ?
548
+ ORDER BY updatedAt DESC
549
+ LIMIT 1
550
+ `),
551
+ findByContentHashShared: db.prepare(`
552
+ SELECT * FROM memories
553
+ WHERE contentHash = ? AND agentId IS NULL
554
+ ORDER BY updatedAt DESC
555
+ LIMIT 1
556
+ `),
557
+ reinforceMemory: db.prepare(`
558
+ UPDATE memories SET reinforcementCount = reinforcementCount + 1, updatedAt = ? WHERE id = ?
559
+ `),
560
+ bumpAccessCount: db.prepare(`
561
+ UPDATE memories SET accessCount = accessCount + 1, lastAccessedAt = ? WHERE id = ?
562
+ `),
498
563
  }
499
564
 
500
565
  function rowToEntry(row: Record<string, unknown>): MemoryEntry {
@@ -517,6 +582,12 @@ function initDb() {
517
582
  image,
518
583
  imagePath: image?.path || undefined,
519
584
  linkedMemoryIds: linkedMemoryIds.length ? linkedMemoryIds : undefined,
585
+ pinned: row.pinned === 1,
586
+ sharedWith: parseJsonSafe<string[]>(row.sharedWith, []).length ? parseJsonSafe<string[]>(row.sharedWith, []) : undefined,
587
+ accessCount: typeof row.accessCount === 'number' ? row.accessCount : 0,
588
+ lastAccessedAt: typeof row.lastAccessedAt === 'number' ? row.lastAccessedAt : 0,
589
+ contentHash: typeof row.contentHash === 'string' ? row.contentHash : undefined,
590
+ reinforcementCount: typeof row.reinforcementCount === 'number' ? row.reinforcementCount : 0,
520
591
  createdAt: typeof row.createdAt === 'number' ? row.createdAt : Date.now(),
521
592
  updatedAt: typeof row.updatedAt === 'number' ? row.updatedAt : Date.now(),
522
593
  }
@@ -556,14 +627,27 @@ function initDb() {
556
627
  const category = data.category || 'note'
557
628
  const title = data.title || 'Untitled'
558
629
  const content = data.content || ''
630
+ const contentHash = computeContentHash(category, content)
631
+
632
+ // Content-hash dedup: if same content already exists for this agent, reinforce instead of duplicating
633
+ const agentId = data.agentId || null
634
+ const existingByHash = agentId
635
+ ? stmts.findByContentHash.get(contentHash, agentId) as Record<string, unknown> | undefined
636
+ : stmts.findByContentHashShared.get(contentHash) as Record<string, unknown> | undefined
637
+ if (existingByHash) {
638
+ stmts.reinforceMemory.run(now, existingByHash.id)
639
+ return rowToEntry({ ...existingByHash, reinforcementCount: ((existingByHash.reinforcementCount as number) || 0) + 1, updatedAt: now })
640
+ }
559
641
 
560
642
  // Guard against exact duplicate memory spam for the same session/category.
561
643
  if (sessionId) {
562
644
  const duplicate = stmts.exactDuplicateBySessionCategory.get(sessionId, category, title, content) as Record<string, unknown> | undefined
563
645
  if (duplicate) return rowToEntry(duplicate)
564
646
  }
647
+ const pinned = data.pinned ? 1 : 0
648
+ const sharedWith = Array.isArray(data.sharedWith) && data.sharedWith.length ? JSON.stringify(data.sharedWith) : null
565
649
  stmts.insert.run(
566
- id, data.agentId || null, sessionId,
650
+ id, agentId, sessionId,
567
651
  category, title, content,
568
652
  data.metadata ? JSON.stringify(data.metadata) : null,
569
653
  null, // embedding computed async
@@ -572,6 +656,9 @@ function initDb() {
572
656
  image ? JSON.stringify(image) : null,
573
657
  image?.path || null,
574
658
  linkedMemoryIds.length ? JSON.stringify(linkedMemoryIds) : null,
659
+ pinned,
660
+ sharedWith,
661
+ contentHash,
575
662
  now, now,
576
663
  )
577
664
  // Compute embedding in background (fire-and-forget)
@@ -601,6 +688,10 @@ function initDb() {
601
688
  image,
602
689
  imagePath: image?.path || null,
603
690
  linkedMemoryIds,
691
+ accessCount: 0,
692
+ lastAccessedAt: 0,
693
+ contentHash,
694
+ reinforcementCount: 0,
604
695
  createdAt: now,
605
696
  updatedAt: now,
606
697
  }
@@ -617,6 +708,8 @@ function initDb() {
617
708
  const nextLinked = normalizeLinkedMemoryIds(merged.linkedMemoryIds, id)
618
709
  const prevLinked = normalizeLinkedMemoryIds(existingEntry.linkedMemoryIds, id)
619
710
  const now = Date.now()
711
+ const pinnedVal = merged.pinned ? 1 : 0
712
+ const sharedWithVal = Array.isArray(merged.sharedWith) && merged.sharedWith.length ? JSON.stringify(merged.sharedWith) : null
620
713
  stmts.update.run(
621
714
  merged.agentId || null, merged.sessionId || null,
622
715
  merged.category, merged.title, merged.content,
@@ -627,6 +720,8 @@ function initDb() {
627
720
  image ? JSON.stringify(image) : null,
628
721
  image?.path || null,
629
722
  nextLinked.length ? JSON.stringify(nextLinked) : null,
723
+ pinnedVal,
724
+ sharedWithVal,
630
725
  now, id,
631
726
  )
632
727
 
@@ -673,6 +768,10 @@ function initDb() {
673
768
  get(id: string): MemoryEntry | null {
674
769
  const row = stmts.getById.get(id) as Record<string, unknown> | undefined
675
770
  if (!row) return null
771
+ // Bump access count (non-blocking)
772
+ setTimeout(() => {
773
+ try { stmts.bumpAccessCount.run(Date.now(), id) } catch { /* best-effort */ }
774
+ }, 0)
676
775
  return rowToEntry(row)
677
776
  },
678
777
 
@@ -755,16 +854,17 @@ function initDb() {
755
854
  search(query: string, agentId?: string): MemoryEntry[] {
756
855
  if (shouldSkipSearchQuery(query)) return []
757
856
  const startedAt = Date.now()
758
- // FTS keyword search
857
+ // FTS keyword search (includes memories shared with this agent)
759
858
  const ftsQuery = buildFtsQuery(query)
760
859
  const ftsResults: MemoryEntry[] = ftsQuery
761
860
  ? (agentId
762
- ? stmts.searchByAgent.all(ftsQuery, agentId) as any[]
861
+ ? stmts.searchByAgentOrShared.all(ftsQuery, agentId, `%"${agentId}"%`) as any[]
763
862
  : stmts.search.all(ftsQuery) as any[]
764
863
  ).map(rowToEntry)
765
864
  : []
766
865
 
767
866
  // Attempt vector search (synchronous — uses cached embedding if available)
867
+ const vectorSimilarityScores = new Map<string, number>()
768
868
  let vectorResults: MemoryEntry[] = []
769
869
  try {
770
870
  const queryEmbedding = getEmbeddingSync(query)
@@ -783,13 +883,17 @@ function initDb() {
783
883
  .sort((a, b) => b.score - a.score)
784
884
  .slice(0, 20)
785
885
 
786
- vectorResults = scored.map((s) => rowToEntry(s.row))
886
+ vectorResults = scored.map((s) => {
887
+ const entry = rowToEntry(s.row)
888
+ vectorSimilarityScores.set(entry.id, s.score)
889
+ return entry
890
+ })
787
891
  }
788
892
  } catch {
789
893
  // Vector search unavailable, use FTS only
790
894
  }
791
895
 
792
- // Merge: deduplicate by id, FTS results first then vector-only
896
+ // Merge: deduplicate by id
793
897
  const seen = new Set<string>()
794
898
  const merged: MemoryEntry[] = []
795
899
  for (const entry of [...ftsResults, ...vectorResults]) {
@@ -798,7 +902,34 @@ function initDb() {
798
902
  merged.push(entry)
799
903
  }
800
904
  }
801
- const out = merged.slice(0, MAX_MERGED_RESULTS)
905
+
906
+ // Apply salience scoring: similarity * recencyDecay * reinforcement * pinnedBoost
907
+ const now = Date.now()
908
+ const HALF_LIFE_DAYS = 30
909
+ const salienceScored = merged.map((entry) => {
910
+ const similarity = vectorSimilarityScores.get(entry.id) ?? 0.5
911
+ const daysSinceAccess = (now - (entry.lastAccessedAt || entry.updatedAt)) / 86_400_000
912
+ const recencyDecay = Math.exp(-0.693 * daysSinceAccess / HALF_LIFE_DAYS)
913
+ const reinforcement = Math.log((entry.reinforcementCount || 0) + 1) + 1
914
+ const pinnedBoost = entry.pinned ? 1.5 : 1.0
915
+ const salience = similarity * recencyDecay * reinforcement * pinnedBoost
916
+ return { entry, salience }
917
+ })
918
+ salienceScored.sort((a, b) => b.salience - a.salience)
919
+
920
+ const out = salienceScored.slice(0, MAX_MERGED_RESULTS).map((s) => s.entry)
921
+
922
+ // Bump access counts for returned results (non-blocking)
923
+ if (out.length) {
924
+ const returnedIds = out.map((e) => e.id)
925
+ setTimeout(() => {
926
+ try {
927
+ const ts = Date.now()
928
+ for (const mid of returnedIds) stmts.bumpAccessCount.run(ts, mid)
929
+ } catch { /* best-effort */ }
930
+ }, 0)
931
+ }
932
+
802
933
  const elapsed = Date.now() - startedAt
803
934
  if (elapsed > 1200) {
804
935
  console.warn(
@@ -838,11 +969,26 @@ function initDb() {
838
969
  list(agentId?: string, limit = 200): MemoryEntry[] {
839
970
  const safeLimit = Math.max(1, Math.min(500, Math.trunc(limit)))
840
971
  const rows = agentId
841
- ? stmts.listByAgent.all(agentId, safeLimit) as any[]
972
+ ? stmts.listByAgentOrShared.all(agentId, `%"${agentId}"%`, safeLimit) as any[]
842
973
  : stmts.listAll.all(safeLimit) as any[]
843
974
  return rows.map(rowToEntry)
844
975
  },
845
976
 
977
+ listPinned(agentId?: string, limit = 20): MemoryEntry[] {
978
+ const safeLimit = Math.max(1, Math.min(100, Math.trunc(limit)))
979
+ const rows = agentId
980
+ ? stmts.listPinnedByAgent.all(agentId, safeLimit) as any[]
981
+ : stmts.listPinnedAll.all(safeLimit) as any[]
982
+ return rows.map(rowToEntry)
983
+ },
984
+
985
+ countsByAgent(): Record<string, number> {
986
+ const rows = stmts.countsByAgent.all() as { agentKey: string; cnt: number }[]
987
+ const result: Record<string, number> = {}
988
+ for (const row of rows) result[row.agentKey] = row.cnt
989
+ return result
990
+ },
991
+
846
992
  getByAgent(agentId: string, limit = 200): MemoryEntry[] {
847
993
  const safeLimit = Math.max(1, Math.min(500, Math.trunc(limit)))
848
994
  return (stmts.listByAgent.all(agentId, safeLimit) as any[]).map(rowToEntry)
@@ -924,9 +1070,32 @@ function initDb() {
924
1070
  const pruneWorking = options.pruneWorking !== false
925
1071
  const cutoff = Date.now() - Math.max(1, Math.min(24 * 365, Math.trunc(options.ttlHours || 24))) * 3600_000
926
1072
 
1073
+ // Hash-based dedup: group by contentHash + agentId, keep the one with highest reinforcementCount
1074
+ if (dedupe && toDelete.size < deleteBudget) {
1075
+ const hashGroups = new Map<string, MemoryEntry[]>()
1076
+ for (const row of rows) {
1077
+ if (!row.contentHash || toDelete.has(row.id)) continue
1078
+ const groupKey = `${row.agentId || ''}|${row.contentHash}`
1079
+ const group = hashGroups.get(groupKey)
1080
+ if (group) group.push(row)
1081
+ else hashGroups.set(groupKey, [row])
1082
+ }
1083
+ for (const group of hashGroups.values()) {
1084
+ if (group.length <= 1) continue
1085
+ group.sort((a, b) => (b.reinforcementCount || 0) - (a.reinforcementCount || 0))
1086
+ for (let i = 1; i < group.length; i++) {
1087
+ toDelete.add(group[i].id)
1088
+ if (toDelete.size >= deleteBudget) break
1089
+ }
1090
+ if (toDelete.size >= deleteBudget) break
1091
+ }
1092
+ }
1093
+
1094
+ // Exact string-match dedup (legacy fallback for rows without contentHash)
927
1095
  if (dedupe) {
928
1096
  const seen = new Set<string>()
929
1097
  for (const row of rows) {
1098
+ if (toDelete.has(row.id)) continue
930
1099
  const key = [
931
1100
  row.agentId || '',
932
1101
  row.sessionId || '',
@@ -1019,6 +1188,8 @@ export function addKnowledge(params: {
1019
1188
  title: string
1020
1189
  content: string
1021
1190
  tags?: string[]
1191
+ scope?: 'global' | 'agent'
1192
+ agentIds?: string[]
1022
1193
  createdByAgentId?: string | null
1023
1194
  createdBySessionId?: string | null
1024
1195
  }): MemoryEntry {
@@ -1031,6 +1202,8 @@ export function addKnowledge(params: {
1031
1202
  content: params.content,
1032
1203
  metadata: {
1033
1204
  tags: params.tags || [],
1205
+ scope: params.scope || 'global',
1206
+ agentIds: params.scope === 'agent' ? (params.agentIds || []) : [],
1034
1207
  createdByAgentId: params.createdByAgentId || null,
1035
1208
  createdBySessionId: params.createdBySessionId || null,
1036
1209
  },
@@ -0,0 +1,51 @@
1
+ export const MIME_TYPES: Record<string, string> = {
2
+ '.png': 'image/png',
3
+ '.jpg': 'image/jpeg',
4
+ '.jpeg': 'image/jpeg',
5
+ '.gif': 'image/gif',
6
+ '.webp': 'image/webp',
7
+ '.svg': 'image/svg+xml',
8
+ '.bmp': 'image/bmp',
9
+ '.ico': 'image/x-icon',
10
+ '.mp4': 'video/mp4',
11
+ '.webm': 'video/webm',
12
+ '.mov': 'video/quicktime',
13
+ '.avi': 'video/x-msvideo',
14
+ '.mkv': 'video/x-matroska',
15
+ '.pdf': 'application/pdf',
16
+ '.json': 'application/json',
17
+ '.csv': 'text/csv',
18
+ '.txt': 'text/plain',
19
+ '.html': 'text/html',
20
+ '.xml': 'application/xml',
21
+ '.zip': 'application/zip',
22
+ '.tar': 'application/x-tar',
23
+ '.gz': 'application/gzip',
24
+ '.doc': 'application/msword',
25
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
26
+ '.xls': 'application/vnd.ms-excel',
27
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
28
+ '.ppt': 'application/vnd.ms-powerpoint',
29
+ '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
30
+ '.mp3': 'audio/mpeg',
31
+ '.wav': 'audio/wav',
32
+ '.ogg': 'audio/ogg',
33
+ }
34
+
35
+ const IMAGE_EXTS = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.bmp', '.ico'])
36
+ const VIDEO_EXTS = new Set(['.mp4', '.webm', '.mov', '.avi', '.mkv'])
37
+ const AUDIO_EXTS = new Set(['.mp3', '.wav', '.ogg'])
38
+ const DOCUMENT_EXTS = new Set(['.pdf', '.json', '.csv', '.txt', '.html', '.xml', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx'])
39
+ const ARCHIVE_EXTS = new Set(['.zip', '.tar', '.gz'])
40
+
41
+ export type FileCategory = 'image' | 'video' | 'audio' | 'document' | 'archive' | 'other'
42
+
43
+ export function getFileCategory(ext: string): FileCategory {
44
+ const lower = ext.toLowerCase()
45
+ if (IMAGE_EXTS.has(lower)) return 'image'
46
+ if (VIDEO_EXTS.has(lower)) return 'video'
47
+ if (AUDIO_EXTS.has(lower)) return 'audio'
48
+ if (DOCUMENT_EXTS.has(lower)) return 'document'
49
+ if (ARCHIVE_EXTS.has(lower)) return 'archive'
50
+ return 'other'
51
+ }
@@ -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 === 1 || this.consecutiveFailures % 5 === 0) {
164
+ console.log(`[openclaw-gateway] ${this.consecutiveFailures} consecutive failure${this.consecutiveFailures > 1 ? 's' : ''}, next retry in ${Math.round(this.reconnectDelay / 1000)}s`)
165
+ }
158
166
  }
159
167
 
160
168
  private rejectAllPending(reason: string) {
@@ -11,6 +11,7 @@ import { buildChatModel } from './build-llm'
11
11
  import { getCheckpointSaver } from './langgraph-checkpoint'
12
12
  import { notify } from './ws-hub'
13
13
  import { pushMainLoopEventToMainSessions } from './main-agent-loop'
14
+ import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
14
15
  import { genId } from '@/lib/id'
15
16
  import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
16
17
  import type { Agent, TaskComment, MessageToolEvent } from '@/types'
@@ -351,6 +352,7 @@ export async function executeLangGraphOrchestrator(
351
352
  const settings = loadSettings()
352
353
  const promptParts: string[] = []
353
354
  if (settings.userPrompt) promptParts.push(settings.userPrompt)
355
+ promptParts.push(buildCurrentDateTimePromptContext())
354
356
  if (orchestrator.soul) promptParts.push(orchestrator.soul)
355
357
  if (orchestrator.systemPrompt) promptParts.push(orchestrator.systemPrompt)
356
358
  // Inject dynamic skills
@@ -6,6 +6,7 @@ import {
6
6
  import { WORKSPACE_DIR } from './data-dir'
7
7
  import { loadRuntimeSettings, getLegacyOrchestratorMaxTurns } from './runtime-settings'
8
8
  import { getMemoryDb } from './memory-db'
9
+ import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
9
10
  import { getProvider } from '../providers'
10
11
  import type { Agent } from '@/types'
11
12
 
@@ -109,6 +110,7 @@ async function executeOrchestratorLegacy(
109
110
  const settings = loadSettings()
110
111
  const promptParts: string[] = []
111
112
  if (settings.userPrompt) promptParts.push(settings.userPrompt)
113
+ promptParts.push(buildCurrentDateTimePromptContext())
112
114
  if (orchestrator.soul) promptParts.push(orchestrator.soul)
113
115
  if (orchestrator.systemPrompt) promptParts.push(orchestrator.systemPrompt)
114
116
  if (orchestrator.skillIds?.length) {
@@ -308,8 +310,8 @@ async function executeSubTask(
308
310
 
309
311
  export async function callProvider(
310
312
  agent: Agent,
311
- systemPrompt: string,
312
- history: { role: string; text: string }[],
313
+ systemPrompt?: string,
314
+ history: { role: string; text: string }[] = [],
313
315
  ): Promise<string> {
314
316
  const provider = getProvider(agent.provider)
315
317
  if (!provider) throw new Error(`Unknown provider: ${agent.provider}`)
@@ -346,6 +348,7 @@ export async function callProvider(
346
348
  session: mockSession,
347
349
  message: history[history.length - 1].text,
348
350
  apiKey,
351
+ systemPrompt,
349
352
  write: (data: string) => {
350
353
  // Parse SSE data to extract text
351
354
  if (data.startsWith('data: ')) {
@@ -7,9 +7,8 @@
7
7
  import { spawn } from 'child_process'
8
8
  import fs from 'fs'
9
9
  import path from 'path'
10
- import os from 'os'
11
10
 
12
- const UPLOAD_DIR = process.env.SWARMCLAW_UPLOAD_DIR || path.join(os.tmpdir(), 'swarmclaw-uploads')
11
+ const UPLOAD_DIR = process.env.SWARMCLAW_UPLOAD_DIR || path.join(process.env.DATA_DIR || path.join(process.cwd(), 'data'), 'uploads')
13
12
  if (!fs.existsSync(UPLOAD_DIR)) fs.mkdirSync(UPLOAD_DIR, { recursive: true })
14
13
 
15
14
  const child = spawn('npx', ['@playwright/mcp@latest'], {
@@ -47,7 +46,7 @@ child.stdout.on('data', (chunk) => {
47
46
  fs.writeFileSync(path.join(UPLOAD_DIR, filename), Buffer.from(block.data, 'base64'))
48
47
  newContent.push({
49
48
  type: 'text',
50
- text: `Screenshot saved. Show it to the user with this markdown: ![Screenshot](/api/uploads/${filename})`,
49
+ text: `Screenshot saved to /api/uploads/${filename} — it is already displayed inline above (do not repeat it with markdown).`,
51
50
  })
52
51
  newContent.push(block) // keep image so Claude can see it
53
52
  } else {
@@ -0,0 +1,53 @@
1
+ function resolveLocalTimezone(): string {
2
+ try {
3
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'
4
+ } catch {
5
+ return 'UTC'
6
+ }
7
+ }
8
+
9
+ function formatDateTimeInTimezone(date: Date, timezone: string): string | null {
10
+ try {
11
+ return new Intl.DateTimeFormat('en-US', {
12
+ weekday: 'long',
13
+ year: 'numeric',
14
+ month: 'long',
15
+ day: 'numeric',
16
+ hour: '2-digit',
17
+ minute: '2-digit',
18
+ second: '2-digit',
19
+ hour12: false,
20
+ timeZone: timezone,
21
+ timeZoneName: 'short',
22
+ }).format(date)
23
+ } catch {
24
+ return null
25
+ }
26
+ }
27
+
28
+ export function buildCurrentDateTimePromptContext(preferredTimezone?: string | null): string {
29
+ const now = new Date()
30
+ const utcIso = now.toISOString()
31
+ const utcFormatted = formatDateTimeInTimezone(now, 'UTC') || utcIso
32
+ const localTimezone = resolveLocalTimezone()
33
+ const requestedTimezone = (preferredTimezone || '').trim()
34
+ const chosenTimezone = requestedTimezone || localTimezone
35
+ const chosenFormatted = formatDateTimeInTimezone(now, chosenTimezone)
36
+
37
+ const lines = [
38
+ '## Runtime Date/Time Context',
39
+ `- Current timestamp (UTC): ${utcIso}`,
40
+ `- Current date/time (UTC): ${utcFormatted}`,
41
+ ]
42
+
43
+ if (chosenFormatted) {
44
+ lines.push(`- Current date/time (${chosenTimezone}): ${chosenFormatted}`)
45
+ } else if (requestedTimezone) {
46
+ lines.push(`- Requested timezone "${requestedTimezone}" could not be resolved. Use UTC time above.`)
47
+ }
48
+
49
+ lines.push('- Treat these as authoritative for terms like "today", "yesterday", "tomorrow", and "recent".')
50
+ lines.push('- For time-sensitive answers, use explicit dates (for example, "March 2, 2026").')
51
+
52
+ return lines.join('\n')
53
+ }