@swarmclawai/swarmclaw 0.6.7 → 0.7.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 (203) hide show
  1. package/README.md +82 -39
  2. package/next.config.ts +31 -6
  3. package/package.json +3 -2
  4. package/src/app/api/agents/[id]/thread/route.ts +1 -0
  5. package/src/app/api/agents/route.ts +19 -5
  6. package/src/app/api/approvals/route.ts +22 -0
  7. package/src/app/api/chatrooms/[id]/chat/route.ts +4 -0
  8. package/src/app/api/clawhub/install/route.ts +2 -2
  9. package/src/app/api/eval/run/route.ts +37 -0
  10. package/src/app/api/eval/scenarios/route.ts +24 -0
  11. package/src/app/api/eval/suite/route.ts +29 -0
  12. package/src/app/api/mcp-servers/[id]/conformance/route.ts +26 -0
  13. package/src/app/api/mcp-servers/[id]/invoke/route.ts +81 -0
  14. package/src/app/api/memory/graph/route.ts +46 -0
  15. package/src/app/api/memory/route.ts +36 -5
  16. package/src/app/api/notifications/route.ts +3 -0
  17. package/src/app/api/plugins/install/route.ts +57 -5
  18. package/src/app/api/plugins/marketplace/route.ts +73 -22
  19. package/src/app/api/plugins/route.ts +61 -1
  20. package/src/app/api/plugins/ui/route.ts +34 -0
  21. package/src/app/api/sessions/[id]/checkpoints/route.ts +31 -0
  22. package/src/app/api/sessions/[id]/restore/route.ts +36 -0
  23. package/src/app/api/settings/route.ts +62 -0
  24. package/src/app/api/setup/doctor/route.ts +22 -5
  25. package/src/app/api/souls/[id]/route.ts +65 -0
  26. package/src/app/api/souls/route.ts +70 -0
  27. package/src/app/api/tasks/[id]/approve/route.ts +4 -3
  28. package/src/app/api/tasks/[id]/route.ts +16 -3
  29. package/src/app/api/tasks/route.ts +10 -2
  30. package/src/app/api/usage/route.ts +9 -2
  31. package/src/app/globals.css +27 -0
  32. package/src/app/page.tsx +10 -5
  33. package/src/cli/index.js +37 -0
  34. package/src/components/activity/activity-feed.tsx +9 -2
  35. package/src/components/agents/agent-avatar.tsx +5 -1
  36. package/src/components/agents/agent-card.tsx +55 -9
  37. package/src/components/agents/agent-sheet.tsx +112 -34
  38. package/src/components/agents/inspector-panel.tsx +1 -1
  39. package/src/components/agents/soul-library-picker.tsx +84 -13
  40. package/src/components/auth/access-key-gate.tsx +63 -54
  41. package/src/components/auth/user-picker.tsx +37 -32
  42. package/src/components/chat/activity-moment.tsx +2 -0
  43. package/src/components/chat/chat-area.tsx +11 -0
  44. package/src/components/chat/chat-header.tsx +69 -25
  45. package/src/components/chat/chat-tool-toggles.tsx +2 -2
  46. package/src/components/chat/checkpoint-timeline.tsx +112 -0
  47. package/src/components/chat/code-block.tsx +3 -1
  48. package/src/components/chat/exec-approval-card.tsx +8 -1
  49. package/src/components/chat/message-bubble.tsx +164 -4
  50. package/src/components/chat/message-list.tsx +46 -4
  51. package/src/components/chat/session-approval-card.tsx +80 -0
  52. package/src/components/chat/session-debug-panel.tsx +106 -84
  53. package/src/components/chat/streaming-bubble.tsx +6 -5
  54. package/src/components/chat/task-approval-card.tsx +78 -0
  55. package/src/components/chat/thinking-indicator.tsx +48 -12
  56. package/src/components/chat/tool-call-bubble.tsx +3 -0
  57. package/src/components/chat/tool-request-banner.tsx +39 -20
  58. package/src/components/chatrooms/chatroom-list.tsx +11 -4
  59. package/src/components/chatrooms/chatroom-sheet.tsx +7 -2
  60. package/src/components/connectors/connector-list.tsx +33 -11
  61. package/src/components/connectors/connector-sheet.tsx +37 -7
  62. package/src/components/home/home-view.tsx +54 -24
  63. package/src/components/input/chat-input.tsx +22 -1
  64. package/src/components/knowledge/knowledge-list.tsx +17 -18
  65. package/src/components/knowledge/knowledge-sheet.tsx +9 -5
  66. package/src/components/layout/app-layout.tsx +87 -19
  67. package/src/components/mcp-servers/mcp-server-list.tsx +352 -50
  68. package/src/components/mcp-servers/mcp-server-sheet.tsx +25 -9
  69. package/src/components/memory/memory-browser.tsx +73 -45
  70. package/src/components/memory/memory-graph-view.tsx +203 -0
  71. package/src/components/memory/memory-list.tsx +20 -13
  72. package/src/components/plugins/plugin-list.tsx +214 -60
  73. package/src/components/plugins/plugin-sheet.tsx +119 -24
  74. package/src/components/projects/project-list.tsx +17 -9
  75. package/src/components/providers/provider-list.tsx +21 -6
  76. package/src/components/providers/provider-sheet.tsx +42 -25
  77. package/src/components/runs/run-list.tsx +17 -13
  78. package/src/components/schedules/schedule-card.tsx +10 -3
  79. package/src/components/schedules/schedule-list.tsx +2 -2
  80. package/src/components/schedules/schedule-sheet.tsx +28 -9
  81. package/src/components/secrets/secret-sheet.tsx +7 -2
  82. package/src/components/secrets/secrets-list.tsx +18 -5
  83. package/src/components/sessions/new-session-sheet.tsx +183 -376
  84. package/src/components/sessions/session-card.tsx +10 -2
  85. package/src/components/settings/gateway-connection-panel.tsx +9 -8
  86. package/src/components/shared/command-palette.tsx +13 -5
  87. package/src/components/shared/empty-state.tsx +20 -8
  88. package/src/components/shared/hint-tip.tsx +31 -0
  89. package/src/components/shared/notification-center.tsx +134 -86
  90. package/src/components/shared/profile-sheet.tsx +4 -0
  91. package/src/components/shared/settings/plugin-manager.tsx +360 -135
  92. package/src/components/shared/settings/section-capability-policy.tsx +3 -3
  93. package/src/components/shared/settings/section-runtime-loop.tsx +149 -4
  94. package/src/components/skills/clawhub-browser.tsx +1 -0
  95. package/src/components/skills/skill-list.tsx +31 -12
  96. package/src/components/skills/skill-sheet.tsx +20 -7
  97. package/src/components/tasks/approvals-panel.tsx +224 -0
  98. package/src/components/tasks/task-board.tsx +20 -12
  99. package/src/components/tasks/task-card.tsx +21 -7
  100. package/src/components/tasks/task-column.tsx +4 -3
  101. package/src/components/tasks/task-list.tsx +1 -1
  102. package/src/components/tasks/task-sheet.tsx +130 -1
  103. package/src/components/ui/dialog.tsx +1 -0
  104. package/src/components/ui/sheet.tsx +1 -0
  105. package/src/components/usage/metrics-dashboard.tsx +72 -48
  106. package/src/components/wallets/wallet-panel.tsx +65 -41
  107. package/src/components/wallets/wallet-section.tsx +9 -3
  108. package/src/components/webhooks/webhook-list.tsx +21 -12
  109. package/src/components/webhooks/webhook-sheet.tsx +13 -3
  110. package/src/lib/approval-display.test.ts +45 -0
  111. package/src/lib/approval-display.ts +62 -0
  112. package/src/lib/clipboard.ts +38 -0
  113. package/src/lib/memory.ts +8 -0
  114. package/src/lib/providers/claude-cli.ts +5 -3
  115. package/src/lib/providers/index.ts +67 -21
  116. package/src/lib/runtime-loop.ts +3 -2
  117. package/src/lib/server/approvals.ts +150 -0
  118. package/src/lib/server/chat-execution.ts +319 -74
  119. package/src/lib/server/chatroom-helpers.ts +63 -5
  120. package/src/lib/server/chatroom-orchestration.ts +74 -0
  121. package/src/lib/server/clawhub-client.ts +82 -6
  122. package/src/lib/server/connectors/manager.ts +27 -1
  123. package/src/lib/server/context-manager.ts +132 -50
  124. package/src/lib/server/cost.test.ts +73 -0
  125. package/src/lib/server/cost.ts +165 -34
  126. package/src/lib/server/daemon-state.ts +112 -1
  127. package/src/lib/server/data-dir.ts +18 -1
  128. package/src/lib/server/eval/runner.ts +126 -0
  129. package/src/lib/server/eval/scenarios.ts +218 -0
  130. package/src/lib/server/eval/scorer.ts +96 -0
  131. package/src/lib/server/eval/store.ts +37 -0
  132. package/src/lib/server/eval/types.ts +48 -0
  133. package/src/lib/server/execution-log.ts +12 -8
  134. package/src/lib/server/guardian.ts +34 -0
  135. package/src/lib/server/heartbeat-service.ts +53 -1
  136. package/src/lib/server/integrity-monitor.ts +208 -0
  137. package/src/lib/server/langgraph-checkpoint.ts +10 -0
  138. package/src/lib/server/link-understanding.ts +55 -0
  139. package/src/lib/server/llm-response-cache.test.ts +102 -0
  140. package/src/lib/server/llm-response-cache.ts +227 -0
  141. package/src/lib/server/main-agent-loop.ts +115 -16
  142. package/src/lib/server/main-session.ts +6 -3
  143. package/src/lib/server/mcp-conformance.test.ts +18 -0
  144. package/src/lib/server/mcp-conformance.ts +233 -0
  145. package/src/lib/server/memory-db.ts +193 -19
  146. package/src/lib/server/memory-retrieval.test.ts +56 -0
  147. package/src/lib/server/mmr.ts +73 -0
  148. package/src/lib/server/orchestrator-lg.ts +7 -1
  149. package/src/lib/server/orchestrator.ts +4 -3
  150. package/src/lib/server/plugins.ts +662 -132
  151. package/src/lib/server/process-manager.ts +18 -0
  152. package/src/lib/server/query-expansion.ts +57 -0
  153. package/src/lib/server/queue.ts +280 -11
  154. package/src/lib/server/runtime-settings.ts +9 -0
  155. package/src/lib/server/session-run-manager.test.ts +23 -0
  156. package/src/lib/server/session-run-manager.ts +32 -2
  157. package/src/lib/server/session-tools/canvas.ts +85 -50
  158. package/src/lib/server/session-tools/chatroom.ts +130 -127
  159. package/src/lib/server/session-tools/connector.ts +233 -454
  160. package/src/lib/server/session-tools/context-mgmt.ts +87 -105
  161. package/src/lib/server/session-tools/crud.ts +84 -7
  162. package/src/lib/server/session-tools/delegate.ts +351 -752
  163. package/src/lib/server/session-tools/discovery.ts +198 -0
  164. package/src/lib/server/session-tools/edit_file.ts +82 -0
  165. package/src/lib/server/session-tools/file-send.test.ts +39 -0
  166. package/src/lib/server/session-tools/file.ts +257 -425
  167. package/src/lib/server/session-tools/git.ts +87 -47
  168. package/src/lib/server/session-tools/http.ts +95 -33
  169. package/src/lib/server/session-tools/index.ts +217 -138
  170. package/src/lib/server/session-tools/memory.ts +154 -239
  171. package/src/lib/server/session-tools/monitor.ts +126 -0
  172. package/src/lib/server/session-tools/normalize-tool-args.test.ts +61 -0
  173. package/src/lib/server/session-tools/normalize-tool-args.ts +48 -0
  174. package/src/lib/server/session-tools/openclaw-nodes.ts +82 -99
  175. package/src/lib/server/session-tools/openclaw-workspace.ts +103 -93
  176. package/src/lib/server/session-tools/platform.ts +86 -0
  177. package/src/lib/server/session-tools/plugin-creator.ts +239 -0
  178. package/src/lib/server/session-tools/sample-ui.ts +97 -0
  179. package/src/lib/server/session-tools/sandbox.ts +175 -148
  180. package/src/lib/server/session-tools/schedule.ts +78 -0
  181. package/src/lib/server/session-tools/session-info.ts +104 -410
  182. package/src/lib/server/session-tools/shell-normalize.test.ts +43 -0
  183. package/src/lib/server/session-tools/shell.ts +171 -143
  184. package/src/lib/server/session-tools/subagent.ts +77 -77
  185. package/src/lib/server/session-tools/wallet.ts +182 -106
  186. package/src/lib/server/session-tools/web.ts +181 -327
  187. package/src/lib/server/storage.ts +36 -0
  188. package/src/lib/server/stream-agent-chat.ts +348 -242
  189. package/src/lib/server/task-quality-gate.test.ts +44 -0
  190. package/src/lib/server/task-quality-gate.ts +67 -0
  191. package/src/lib/server/task-validation.test.ts +78 -0
  192. package/src/lib/server/task-validation.ts +67 -2
  193. package/src/lib/server/tool-aliases.ts +68 -0
  194. package/src/lib/server/tool-capability-policy.ts +24 -5
  195. package/src/lib/server/tool-retry.ts +62 -0
  196. package/src/lib/server/transcript-repair.ts +72 -0
  197. package/src/lib/setup-defaults.ts +1 -0
  198. package/src/lib/tasks.ts +7 -1
  199. package/src/lib/tool-definitions.ts +24 -23
  200. package/src/lib/validation/schemas.ts +13 -0
  201. package/src/lib/view-routes.ts +2 -23
  202. package/src/stores/use-app-store.ts +23 -1
  203. package/src/types/index.ts +155 -10
@@ -5,6 +5,7 @@ import { createHash } from 'crypto'
5
5
  import { genId } from '@/lib/id'
6
6
  import type { MemoryEntry, FileReference, MemoryImage, MemoryReference } from '@/types'
7
7
  import { getEmbedding, cosineSimilarity, serializeEmbedding, deserializeEmbedding } from './embeddings'
8
+ import { applyMMR } from './mmr'
8
9
  import { loadSettings } from './storage'
9
10
  import {
10
11
  normalizeLinkedMemoryIds,
@@ -23,8 +24,8 @@ const MAX_IMAGE_INPUT_BYTES = 10 * 1024 * 1024 // 10MB
23
24
  const IMAGE_EXT_WHITELIST = new Set(['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.tiff'])
24
25
  export const MAX_FTS_QUERY_TERMS = 6
25
26
  export const MAX_FTS_TERM_LENGTH = 48
26
- const MAX_FTS_RESULT_ROWS = 30
27
- const MAX_MERGED_RESULTS = 50
27
+ const MAX_FTS_RESULT_ROWS = 50
28
+ const MAX_MERGED_RESULTS = 80
28
29
 
29
30
  export const MEMORY_FTS_STOP_WORDS = new Set([
30
31
  'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'for', 'from', 'how',
@@ -33,6 +34,140 @@ export const MEMORY_FTS_STOP_WORDS = new Set([
33
34
  'you', 'your',
34
35
  ])
35
36
 
37
+ export type MemoryScopeMode = 'auto' | 'all' | 'global' | 'agent' | 'session' | 'project'
38
+ export type MemoryRerankMode = 'balanced' | 'semantic' | 'lexical'
39
+
40
+ export interface MemoryScopeFilter {
41
+ mode: MemoryScopeMode
42
+ agentId?: string | null
43
+ sessionId?: string | null
44
+ projectRoot?: string | null
45
+ }
46
+
47
+ export interface MemorySearchOptions {
48
+ scope?: MemoryScopeFilter
49
+ rerankMode?: MemoryRerankMode
50
+ }
51
+
52
+ function normalizeScopeIdentifier(value: unknown): string | null {
53
+ if (typeof value !== 'string') return null
54
+ const trimmed = value.trim()
55
+ return trimmed ? trimmed : null
56
+ }
57
+
58
+ function normalizePathForScope(value: unknown): string | null {
59
+ if (typeof value !== 'string') return null
60
+ const trimmed = value.trim()
61
+ if (!trimmed) return null
62
+ return path.normalize(trimmed).replace(/\\/g, '/').replace(/\/+$/, '').toLowerCase()
63
+ }
64
+
65
+ function memorySearchText(entry: MemoryEntry): string {
66
+ const refs = Array.isArray(entry.references)
67
+ ? entry.references.map((ref) => `${ref.type} ${ref.path || ''} ${ref.title || ''} ${ref.note || ''}`).join(' ')
68
+ : ''
69
+ return `${entry.title || ''} ${entry.content || ''} ${refs}`.toLowerCase()
70
+ }
71
+
72
+ function tokenizeForRerank(input: string): string[] {
73
+ const raw = String(input || '').toLowerCase().match(/[a-z0-9][a-z0-9._:/-]*/g) || []
74
+ const out: string[] = []
75
+ const seen = new Set<string>()
76
+ for (const token of raw) {
77
+ if (token.length < 2) continue
78
+ if (MEMORY_FTS_STOP_WORDS.has(token)) continue
79
+ if (seen.has(token)) continue
80
+ seen.add(token)
81
+ out.push(token)
82
+ }
83
+ return out
84
+ }
85
+
86
+ function keywordOverlapScore(queryTokens: string[], entryText: string): number {
87
+ if (!queryTokens.length) return 0
88
+ const corpus = entryText.toLowerCase()
89
+ let matched = 0
90
+ for (const token of queryTokens) {
91
+ if (token.length < 3) continue
92
+ if (corpus.includes(token)) matched++
93
+ }
94
+ return matched / queryTokens.length
95
+ }
96
+
97
+ function entryRootsForScope(entry: MemoryEntry): string[] {
98
+ const roots = new Set<string>()
99
+ const add = (raw: unknown) => {
100
+ const normalized = normalizePathForScope(raw)
101
+ if (normalized) roots.add(normalized)
102
+ }
103
+
104
+ add((entry.metadata as Record<string, unknown> | undefined)?.projectRoot)
105
+
106
+ if (Array.isArray(entry.references)) {
107
+ for (const ref of entry.references) {
108
+ add(ref.projectRoot)
109
+ if (ref.type === 'project') add(ref.path)
110
+ if (ref.type === 'folder' || ref.type === 'file') add(ref.path)
111
+ }
112
+ }
113
+
114
+ if (Array.isArray(entry.filePaths)) {
115
+ for (const ref of entry.filePaths) {
116
+ add(ref.projectRoot)
117
+ add(ref.path)
118
+ }
119
+ }
120
+
121
+ return [...roots]
122
+ }
123
+
124
+ function scopeAllowsAgentAccess(entry: MemoryEntry, agentId: string): boolean {
125
+ if (entry.agentId === agentId) return true
126
+ if (Array.isArray(entry.sharedWith) && entry.sharedWith.includes(agentId)) return true
127
+ return false
128
+ }
129
+
130
+ export function normalizeMemoryScopeMode(raw: unknown): MemoryScopeMode {
131
+ const value = typeof raw === 'string' ? raw.trim().toLowerCase() : ''
132
+ if (value === 'shared') return 'global'
133
+ if (value === 'all' || value === 'global' || value === 'agent' || value === 'session' || value === 'project') return value
134
+ return 'auto'
135
+ }
136
+
137
+ export function filterMemoriesByScope(entries: MemoryEntry[], scope?: MemoryScopeFilter): MemoryEntry[] {
138
+ if (!scope || scope.mode === 'all') return entries
139
+ const mode = normalizeMemoryScopeMode(scope.mode)
140
+ const agentId = normalizeScopeIdentifier(scope.agentId)
141
+ const sessionId = normalizeScopeIdentifier(scope.sessionId)
142
+ const projectRoot = normalizePathForScope(scope.projectRoot)
143
+
144
+ if (mode === 'global') {
145
+ return entries.filter((entry) => !entry.agentId)
146
+ }
147
+
148
+ if (mode === 'agent') {
149
+ if (!agentId) return []
150
+ return entries.filter((entry) => scopeAllowsAgentAccess(entry, agentId))
151
+ }
152
+
153
+ if (mode === 'session') {
154
+ if (!sessionId) return []
155
+ return entries.filter((entry) => entry.sessionId === sessionId)
156
+ }
157
+
158
+ if (mode === 'project') {
159
+ if (!projectRoot) return []
160
+ return entries.filter((entry) => {
161
+ const roots = entryRootsForScope(entry)
162
+ return roots.some((root) => root === projectRoot || root.startsWith(`${projectRoot}/`))
163
+ })
164
+ }
165
+
166
+ // auto
167
+ if (!agentId) return entries
168
+ return entries.filter((entry) => !entry.agentId || scopeAllowsAgentAccess(entry, agentId))
169
+ }
170
+
36
171
  function computeContentHash(category: string, content: string): string {
37
172
  const normalized = `${category}|${content.toLowerCase().trim()}`
38
173
  return createHash('sha256').update(normalized).digest('hex').slice(0, 16)
@@ -611,8 +746,8 @@ function initDb() {
611
746
  const getAllWithEmbeddings = db.prepare(
612
747
  `SELECT * FROM memories WHERE embedding IS NOT NULL`
613
748
  )
614
- const getAllWithEmbeddingsByAgent = db.prepare(
615
- `SELECT * FROM memories WHERE embedding IS NOT NULL AND agentId = ?`
749
+ const getAllWithEmbeddingsByAgentOrShared = db.prepare(
750
+ `SELECT * FROM memories WHERE embedding IS NOT NULL AND (agentId = ? OR sharedWith LIKE ?)`
616
751
  )
617
752
 
618
753
  return {
@@ -851,33 +986,54 @@ function initDb() {
851
986
  return this.get(id)
852
987
  },
853
988
 
854
- search(query: string, agentId?: string): MemoryEntry[] {
989
+ search(query: string, agentId?: string, options: MemorySearchOptions = {}): MemoryEntry[] {
855
990
  if (shouldSkipSearchQuery(query)) return []
856
991
  const startedAt = Date.now()
992
+ const normalizedAgentId = normalizeScopeIdentifier(agentId)
993
+ const rerankMode: MemoryRerankMode = options.rerankMode === 'semantic' || options.rerankMode === 'lexical'
994
+ ? options.rerankMode
995
+ : 'balanced'
996
+ const scopeMode = options.scope
997
+ ? normalizeMemoryScopeMode(options.scope.mode)
998
+ : (normalizedAgentId ? 'agent' : 'all')
999
+ const scopeFilter: MemoryScopeFilter | undefined = scopeMode === 'all'
1000
+ ? undefined
1001
+ : {
1002
+ mode: scopeMode,
1003
+ agentId: options.scope?.agentId ?? normalizedAgentId,
1004
+ sessionId: options.scope?.sessionId,
1005
+ projectRoot: options.scope?.projectRoot,
1006
+ }
1007
+
857
1008
  // FTS keyword search (includes memories shared with this agent)
858
1009
  const ftsQuery = buildFtsQuery(query)
1010
+ const fastAgentOnlyScope = scopeMode === 'agent' && !!normalizedAgentId
859
1011
  const ftsResults: MemoryEntry[] = ftsQuery
860
- ? (agentId
861
- ? stmts.searchByAgentOrShared.all(ftsQuery, agentId, `%"${agentId}"%`) as any[]
1012
+ ? (fastAgentOnlyScope
1013
+ ? stmts.searchByAgentOrShared.all(ftsQuery, normalizedAgentId, `%"${normalizedAgentId}"%`) as any[]
862
1014
  : stmts.search.all(ftsQuery) as any[]
863
1015
  ).map(rowToEntry)
864
1016
  : []
1017
+ const ftsHitIds = new Set<string>(ftsResults.map((entry) => entry.id))
865
1018
 
866
1019
  // Attempt vector search (synchronous — uses cached embedding if available)
867
1020
  const vectorSimilarityScores = new Map<string, number>()
1021
+ const rawEmbeddings = new Map<string, number[]>()
868
1022
  let vectorResults: MemoryEntry[] = []
1023
+ let queryEmbeddingResult: number[] | undefined
869
1024
  try {
870
1025
  const queryEmbedding = getEmbeddingSync(query)
1026
+ queryEmbeddingResult = queryEmbedding || undefined
871
1027
  if (queryEmbedding) {
872
- const rows = agentId
873
- ? getAllWithEmbeddingsByAgent.all(agentId) as any[]
1028
+ const rows = fastAgentOnlyScope
1029
+ ? getAllWithEmbeddingsByAgentOrShared.all(normalizedAgentId, `%"${normalizedAgentId}"%`) as any[]
874
1030
  : getAllWithEmbeddings.all() as any[]
875
1031
 
876
1032
  const scored = rows
877
1033
  .map((row) => {
878
1034
  const emb = deserializeEmbedding(row.embedding)
879
1035
  const score = cosineSimilarity(queryEmbedding, emb)
880
- return { row, score }
1036
+ return { row, score, emb }
881
1037
  })
882
1038
  .filter((s) => s.score > 0.3) // relevance threshold
883
1039
  .sort((a, b) => b.score - a.score)
@@ -886,6 +1042,7 @@ function initDb() {
886
1042
  vectorResults = scored.map((s) => {
887
1043
  const entry = rowToEntry(s.row)
888
1044
  vectorSimilarityScores.set(entry.id, s.score)
1045
+ rawEmbeddings.set(entry.id, s.emb)
889
1046
  return entry
890
1047
  })
891
1048
  }
@@ -902,22 +1059,38 @@ function initDb() {
902
1059
  merged.push(entry)
903
1060
  }
904
1061
  }
1062
+ const scopedMerged = scopeFilter ? filterMemoriesByScope(merged, scopeFilter) : merged
905
1063
 
906
- // Apply salience scoring: similarity * recencyDecay * reinforcement * pinnedBoost
1064
+ // Retrieval v2 rerank: hybrid relevance (semantic + lexical + FTS signal), then salience decay/boosting.
1065
+ const queryTokens = tokenizeForRerank(query)
907
1066
  const now = Date.now()
908
1067
  const HALF_LIFE_DAYS = 30
909
- const salienceScored = merged.map((entry) => {
910
- const similarity = vectorSimilarityScores.get(entry.id) ?? 0.5
1068
+ const salienceScored = scopedMerged.map((entry) => {
1069
+ const semantic = vectorSimilarityScores.get(entry.id) ?? (ftsHitIds.has(entry.id) ? 0.42 : 0.18)
1070
+ const lexical = keywordOverlapScore(queryTokens, memorySearchText(entry))
1071
+ const ftsSignal = ftsHitIds.has(entry.id) ? 1 : 0
1072
+ const relevance = rerankMode === 'semantic'
1073
+ ? (semantic * 0.78 + ftsSignal * 0.22)
1074
+ : rerankMode === 'lexical'
1075
+ ? (lexical * 0.78 + ftsSignal * 0.22)
1076
+ : (semantic * 0.50 + lexical * 0.35 + ftsSignal * 0.15)
911
1077
  const daysSinceAccess = (now - (entry.lastAccessedAt || entry.updatedAt)) / 86_400_000
912
1078
  const recencyDecay = Math.exp(-0.693 * daysSinceAccess / HALF_LIFE_DAYS)
913
1079
  const reinforcement = Math.log((entry.reinforcementCount || 0) + 1) + 1
914
1080
  const pinnedBoost = entry.pinned ? 1.5 : 1.0
915
- const salience = similarity * recencyDecay * reinforcement * pinnedBoost
916
- return { entry, salience }
1081
+ const salience = Math.max(0.0001, relevance) * recencyDecay * reinforcement * pinnedBoost
1082
+ return { entry, salience, embedding: rawEmbeddings.get(entry.id) }
917
1083
  })
918
- salienceScored.sort((a, b) => b.salience - a.salience)
919
1084
 
920
- const out = salienceScored.slice(0, MAX_MERGED_RESULTS).map((s) => s.entry)
1085
+ // Apply MMR when semantic reranking is enabled and query embeddings are available.
1086
+ let out: MemoryEntry[] = []
1087
+ const useMmr = !!queryEmbeddingResult && rerankMode !== 'lexical'
1088
+ if (useMmr && queryEmbeddingResult) {
1089
+ out = applyMMR(queryEmbeddingResult, salienceScored, MAX_MERGED_RESULTS, 0.6)
1090
+ } else {
1091
+ salienceScored.sort((a, b) => b.salience - a.salience)
1092
+ out = salienceScored.slice(0, MAX_MERGED_RESULTS).map((s) => s.entry)
1093
+ }
921
1094
 
922
1095
  // Bump access counts for returned results (non-blocking)
923
1096
  if (out.length) {
@@ -933,7 +1106,7 @@ function initDb() {
933
1106
  const elapsed = Date.now() - startedAt
934
1107
  if (elapsed > 1200) {
935
1108
  console.warn(
936
- `[memory-db] Slow search ${elapsed}ms (agent=${agentId || 'all'}, rawLen=${String(query || '').length}, fts="${ftsQuery.slice(0, 180)}")`,
1109
+ `[memory-db] Slow search ${elapsed}ms (scope=${scopeMode}, rerank=${rerankMode}, rawLen=${String(query || '').length}, fts="${ftsQuery.slice(0, 180)}")`,
937
1110
  )
938
1111
  }
939
1112
  return out
@@ -946,8 +1119,9 @@ function initDb() {
946
1119
  maxDepth?: number,
947
1120
  maxResults?: number,
948
1121
  maxLinkedExpansion?: number,
1122
+ options: MemorySearchOptions = {},
949
1123
  ): { entries: MemoryEntry[]; truncated: boolean; expandedLinkedCount: number; limits: MemoryLookupLimits } {
950
- const baseResults = this.search(query, agentId)
1124
+ const baseResults = this.search(query, agentId, options)
951
1125
  const defaults = getMemoryLookupLimits()
952
1126
  const limits = resolveLookupRequest(defaults, {
953
1127
  depth: maxDepth ?? defaults.maxDepth,
@@ -0,0 +1,56 @@
1
+ import assert from 'node:assert/strict'
2
+ import { test } from 'node:test'
3
+ import type { MemoryEntry } from '@/types'
4
+ import { filterMemoriesByScope, normalizeMemoryScopeMode } from './memory-db.ts'
5
+
6
+ function makeEntry(id: string, patch: Partial<MemoryEntry> = {}): MemoryEntry {
7
+ return {
8
+ id,
9
+ agentId: null,
10
+ sessionId: null,
11
+ category: 'note',
12
+ title: `Entry ${id}`,
13
+ content: `content ${id}`,
14
+ createdAt: 1,
15
+ updatedAt: 1,
16
+ ...patch,
17
+ }
18
+ }
19
+
20
+ test('normalizeMemoryScopeMode maps shared alias to global', () => {
21
+ assert.equal(normalizeMemoryScopeMode('shared'), 'global')
22
+ assert.equal(normalizeMemoryScopeMode('project'), 'project')
23
+ assert.equal(normalizeMemoryScopeMode('unknown'), 'auto')
24
+ })
25
+
26
+ test('filterMemoriesByScope auto includes global + own + shared-with-agent', () => {
27
+ const rows: MemoryEntry[] = [
28
+ makeEntry('global', { agentId: null }),
29
+ makeEntry('mine', { agentId: 'agent-a' }),
30
+ makeEntry('shared', { agentId: 'agent-b', sharedWith: ['agent-a'] }),
31
+ makeEntry('other', { agentId: 'agent-b' }),
32
+ ]
33
+ const filtered = filterMemoriesByScope(rows, { mode: 'auto', agentId: 'agent-a' })
34
+ assert.deepEqual(filtered.map((r) => r.id), ['global', 'mine', 'shared'])
35
+ })
36
+
37
+ test('filterMemoriesByScope supports project and session scopes', () => {
38
+ const rows: MemoryEntry[] = [
39
+ makeEntry('proj-hit', {
40
+ sessionId: 's-1',
41
+ references: [{ type: 'project', path: '/repo/swarm', projectRoot: '/repo/swarm', timestamp: 1 }],
42
+ }),
43
+ makeEntry('proj-miss', {
44
+ sessionId: 's-1',
45
+ references: [{ type: 'project', path: '/repo/other', projectRoot: '/repo/other', timestamp: 1 }],
46
+ }),
47
+ makeEntry('session-hit', { sessionId: 's-2' }),
48
+ ]
49
+
50
+ const projectFiltered = filterMemoriesByScope(rows, { mode: 'project', projectRoot: '/repo/swarm' })
51
+ assert.deepEqual(projectFiltered.map((r) => r.id), ['proj-hit'])
52
+
53
+ const sessionFiltered = filterMemoriesByScope(rows, { mode: 'session', sessionId: 's-2' })
54
+ assert.deepEqual(sessionFiltered.map((r) => r.id), ['session-hit'])
55
+ })
56
+
@@ -0,0 +1,73 @@
1
+ import { cosineSimilarity } from './embeddings'
2
+ import type { MemoryEntry } from '@/types'
3
+
4
+ /**
5
+ * Applies Maximal Marginal Relevance (MMR) to diversify search results.
6
+ * It balances relevance to the query (salience/similarity) against novelty
7
+ * compared to already-selected documents.
8
+ */
9
+ export function applyMMR(
10
+ queryEmbedding: number[],
11
+ candidates: Array<{ entry: MemoryEntry; salience: number; embedding?: number[] }>,
12
+ limit: number,
13
+ lambda: number = 0.5
14
+ ): MemoryEntry[] {
15
+ if (candidates.length === 0) return []
16
+
17
+ // Normalize salience to [0, 1] range
18
+ const maxSalience = Math.max(...candidates.map(c => c.salience))
19
+ const minSalience = Math.min(...candidates.map(c => c.salience))
20
+ const salienceRange = maxSalience - minSalience || 1
21
+
22
+ const candidatesWithNormalizedSalience = candidates.map(c => ({
23
+ ...c,
24
+ normSalience: (c.salience - minSalience) / salienceRange
25
+ }))
26
+
27
+ const selected: typeof candidatesWithNormalizedSalience = []
28
+ const remaining = [...candidatesWithNormalizedSalience]
29
+
30
+ // Debug: uncomment for troubleshooting
31
+ // console.log(`[mmr] Starting MMR for ${remaining.length} candidates, limit=${limit}, lambda=${lambda}`)
32
+
33
+ while (selected.length < limit && remaining.length > 0) {
34
+ let bestMmrScore = -Infinity
35
+ let bestIndex = -1
36
+
37
+ for (let i = 0; i < remaining.length; i++) {
38
+ const candidate = remaining[i]
39
+
40
+ let maxSimToSelected = 0
41
+ if (selected.length > 0 && candidate.embedding) {
42
+ for (const sel of selected) {
43
+ if (sel.embedding) {
44
+ const sim = cosineSimilarity(candidate.embedding, sel.embedding)
45
+ if (sim > maxSimToSelected) maxSimToSelected = sim
46
+ }
47
+ }
48
+ }
49
+
50
+ // MMR Score = Lambda * Relevance - (1 - Lambda) * Diversity (max similarity to selected)
51
+ const mmrScore = (lambda * candidate.normSalience) - ((1 - lambda) * maxSimToSelected)
52
+
53
+ // DEBUG LOG
54
+ // console.log(` Candidate ${candidate.entry.id}: rel=${candidate.normSalience.toFixed(3)}, div_penalty=${maxSimToSelected.toFixed(3)}, mmr=${mmrScore.toFixed(3)}`)
55
+
56
+ if (mmrScore > bestMmrScore) {
57
+ bestMmrScore = mmrScore
58
+ bestIndex = i
59
+ }
60
+ }
61
+
62
+ if (bestIndex !== -1) {
63
+ const picked = remaining[bestIndex]
64
+ // console.log(`[mmr] Picked ${picked.entry.id} with score ${bestMmrScore.toFixed(3)}`)
65
+ selected.push(picked)
66
+ remaining.splice(bestIndex, 1)
67
+ } else {
68
+ break
69
+ }
70
+ }
71
+
72
+ return selected.map(s => s.entry)
73
+ }
@@ -12,6 +12,7 @@ import { getCheckpointSaver } from './langgraph-checkpoint'
12
12
  import { notify } from './ws-hub'
13
13
  import { pushMainLoopEventToMainSessions } from './main-agent-loop'
14
14
  import { buildCurrentDateTimePromptContext } from './prompt-runtime-context'
15
+ import { getPluginManager } from './plugins'
15
16
  import { genId } from '@/lib/id'
16
17
  import { NON_LANGGRAPH_PROVIDER_IDS } from '@/lib/provider-sets'
17
18
  import type { Agent, TaskComment, MessageToolEvent } from '@/types'
@@ -176,6 +177,7 @@ export async function executeLangGraphOrchestrator(
176
177
  return `Agent "${agentName}" not found. Available: ${agents.map((a) => a.name).join(', ')}`
177
178
  }
178
179
  console.log(`[orchestrator-lg] Delegating to ${agent.name}: ${agentTask.slice(0, 80)}`)
180
+ getPluginManager().runHook('onAgentDelegation', { sourceAgentId: orchestrator.id, targetAgentId: agent.id, task: agentTask })
179
181
  const result = await executeSubTaskViaCli(agent, agentTask, sessionId)
180
182
  saveMessage(sessionId, 'assistant', `Delegated to ${agent.name}: ${agentTask.slice(0, 100)}`, [{
181
183
  name: 'delegate_to_agent',
@@ -602,7 +604,10 @@ export async function executeLangGraphOrchestrator(
602
604
 
603
605
  // Extract summary from mark_complete if present
604
606
  const completeMatch = finalResult.match(/ORCHESTRATION_COMPLETE:\s*([\s\S]+)/)
605
- return completeMatch ? completeMatch[1].trim() : finalResult
607
+ const summary = completeMatch ? completeMatch[1].trim() : finalResult
608
+
609
+ // Append meta so the main-loop knows we are done
610
+ return `${summary}\n\n[MAIN_LOOP_META] {"status":"ok", "follow_up":false, "summary":${JSON.stringify(summary.slice(0, 300))}, "mission_task_id":${JSON.stringify(taskId || null)}}`
606
611
  }
607
612
 
608
613
  /**
@@ -623,6 +628,7 @@ export async function resumeLangGraphOrchestrator(
623
628
  async ({ agentName, task: agentTask }) => {
624
629
  const agent = agents.find((a) => a.name.toLowerCase() === agentName.toLowerCase())
625
630
  if (!agent) return `Agent "${agentName}" not found. Available: ${agents.map((a) => a.name).join(', ')}`
631
+ getPluginManager().runHook('onAgentDelegation', { sourceAgentId: orchestrator.id, targetAgentId: agent.id, task: agentTask })
626
632
  const result = await executeSubTaskViaCli(agent, agentTask, sessionId)
627
633
  saveMessage(sessionId, 'assistant', `Delegated to ${agent.name}: ${agentTask.slice(0, 100)}`, [{
628
634
  name: 'delegate_to_agent',
@@ -77,13 +77,14 @@ export async function executeOrchestrator(
77
77
 
78
78
  // claude-cli fallback (no structured tool calling)
79
79
  console.warn(`[orchestrator] Using legacy regex-based engine for ${orchestrator.name} (claude-cli)`)
80
- return executeOrchestratorLegacy(orchestrator, task, sessionId)
80
+ return executeOrchestratorLegacy(orchestrator, task, sessionId, taskId)
81
81
  }
82
82
 
83
83
  async function executeOrchestratorLegacy(
84
84
  orchestrator: Agent,
85
85
  task: string,
86
86
  sessionId: string,
87
+ taskId?: string,
87
88
  ): Promise<string> {
88
89
  const allAgents = loadAgents()
89
90
  const sessions = loadSessions()
@@ -240,7 +241,7 @@ async function executeOrchestratorLegacy(
240
241
 
241
242
  if (cmd.done) {
242
243
  result = cmd.summary || fullText
243
- return result
244
+ return `${result}\n\n[MAIN_LOOP_META] {"status":"ok", "follow_up":false, "summary":${JSON.stringify(result.slice(0, 300))}, "mission_task_id":${JSON.stringify(taskId || null)}}`
244
245
  }
245
246
  }
246
247
 
@@ -255,7 +256,7 @@ async function executeOrchestratorLegacy(
255
256
  result = `Loop stopped after reaching max turns (${maxTurns}).`
256
257
  }
257
258
 
258
- return result
259
+ return `${result}\n\n[MAIN_LOOP_META] {"status":"ok", "follow_up":false, "summary":${JSON.stringify(result.slice(0, 300))}, "mission_task_id":${JSON.stringify(taskId || null)}}`
259
260
  }
260
261
 
261
262
  async function executeSubTask(