swarm_sdk 2.7.14 → 3.0.0.alpha2

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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
  4. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  5. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  6. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  7. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  8. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  9. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  10. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  11. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  12. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  13. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  14. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  15. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  16. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  17. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  18. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  19. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  20. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  24. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  25. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  26. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  27. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  28. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  29. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  30. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  31. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  32. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  33. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  34. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  35. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  36. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  37. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  38. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  39. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  40. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  41. data/lib/swarm_sdk/v3/tools/document_converters/base.rb +84 -0
  42. data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
  43. data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -0
  45. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  46. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  47. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  48. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  49. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  50. data/lib/swarm_sdk/v3/tools/read.rb +213 -0
  51. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  52. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  53. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  54. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  55. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  56. data/lib/swarm_sdk/v3.rb +145 -0
  57. metadata +88 -149
  58. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  59. data/lib/swarm_sdk/agent/builder.rb +0 -705
  60. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  61. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  62. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  63. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  64. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  65. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  66. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  67. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  68. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  69. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  70. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  71. data/lib/swarm_sdk/agent/context.rb +0 -115
  72. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  73. data/lib/swarm_sdk/agent/definition.rb +0 -588
  74. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  75. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  76. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  77. data/lib/swarm_sdk/agent_registry.rb +0 -146
  78. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  79. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  80. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  81. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  82. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  83. data/lib/swarm_sdk/config.rb +0 -368
  84. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  85. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  86. data/lib/swarm_sdk/configuration.rb +0 -165
  87. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  88. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  89. data/lib/swarm_sdk/context_compactor.rb +0 -335
  90. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  91. data/lib/swarm_sdk/context_management/context.rb +0 -328
  92. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  93. data/lib/swarm_sdk/defaults.rb +0 -251
  94. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  95. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  96. data/lib/swarm_sdk/hooks/context.rb +0 -197
  97. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  98. data/lib/swarm_sdk/hooks/error.rb +0 -29
  99. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  100. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  101. data/lib/swarm_sdk/hooks/result.rb +0 -150
  102. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  103. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  104. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  105. data/lib/swarm_sdk/log_collector.rb +0 -227
  106. data/lib/swarm_sdk/log_stream.rb +0 -127
  107. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  108. data/lib/swarm_sdk/model_aliases.json +0 -8
  109. data/lib/swarm_sdk/models.json +0 -44002
  110. data/lib/swarm_sdk/models.rb +0 -161
  111. data/lib/swarm_sdk/node_context.rb +0 -245
  112. data/lib/swarm_sdk/observer/builder.rb +0 -81
  113. data/lib/swarm_sdk/observer/config.rb +0 -45
  114. data/lib/swarm_sdk/observer/manager.rb +0 -248
  115. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  116. data/lib/swarm_sdk/permissions/config.rb +0 -239
  117. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  118. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  119. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  120. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  121. data/lib/swarm_sdk/plugin.rb +0 -309
  122. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  123. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  124. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  125. data/lib/swarm_sdk/restore_result.rb +0 -65
  126. data/lib/swarm_sdk/result.rb +0 -241
  127. data/lib/swarm_sdk/snapshot.rb +0 -156
  128. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  129. data/lib/swarm_sdk/state_restorer.rb +0 -476
  130. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  131. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  132. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  133. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  134. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  135. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  136. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  141. data/lib/swarm_sdk/swarm.rb +0 -973
  142. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  143. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  144. data/lib/swarm_sdk/tools/base.rb +0 -63
  145. data/lib/swarm_sdk/tools/bash.rb +0 -280
  146. data/lib/swarm_sdk/tools/clock.rb +0 -46
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  148. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  149. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  150. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  151. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  152. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  153. data/lib/swarm_sdk/tools/edit.rb +0 -145
  154. data/lib/swarm_sdk/tools/glob.rb +0 -166
  155. data/lib/swarm_sdk/tools/grep.rb +0 -235
  156. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  157. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  160. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  161. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  162. data/lib/swarm_sdk/tools/read.rb +0 -261
  163. data/lib/swarm_sdk/tools/registry.rb +0 -205
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  166. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  167. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  168. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  169. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  170. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  171. data/lib/swarm_sdk/tools/think.rb +0 -100
  172. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  173. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  174. data/lib/swarm_sdk/tools/write.rb +0 -112
  175. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  176. data/lib/swarm_sdk/utils.rb +0 -68
  177. data/lib/swarm_sdk/validation_result.rb +0 -33
  178. data/lib/swarm_sdk/version.rb +0 -5
  179. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  180. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  181. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  182. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  183. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  184. data/lib/swarm_sdk/workflow.rb +0 -589
  185. data/lib/swarm_sdk.rb +0 -721
@@ -0,0 +1,427 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module V3
5
+ module Memory
6
+ # Periodic maintenance for the memory system
7
+ #
8
+ # Performs:
9
+ # - Deduplication and canonicalization of similar cards
10
+ # - Cluster summary updates
11
+ # - Conflict detection between contradicting cards
12
+ # - Merging of redundant cards
13
+ #
14
+ # @example
15
+ # consolidator = Consolidator.new(adapter: adapter, embedder: embedder)
16
+ # consolidator.run
17
+ class Consolidator
18
+ # Edge types that indicate a supporting relationship between cards.
19
+ # Pairs connected by these edges should never be flagged as contradictions.
20
+ # Derived from Edge::TYPES so new edge types are non-conflicting by default.
21
+ SUPPORTING_EDGE_TYPES = (Edge::TYPES - [:contradicts]).freeze
22
+
23
+ # @param adapter [Adapters::Base] Storage adapter
24
+ # @param embedder [Embedder] Text embedder
25
+ def initialize(adapter:, embedder:)
26
+ @adapter = adapter
27
+ @embedder = embedder
28
+ @config = Configuration.instance
29
+ end
30
+
31
+ # Run full consolidation
32
+ #
33
+ # @return [Hash] Summary of actions taken
34
+ def run
35
+ deduped = deduplicate
36
+ conflicts = detect_conflicts
37
+ clusters_updated = update_cluster_summaries
38
+
39
+ {
40
+ duplicates_merged: deduped,
41
+ conflicts_detected: conflicts,
42
+ clusters_updated: clusters_updated,
43
+ }
44
+ end
45
+
46
+ # Run full consolidation with progress events
47
+ #
48
+ # @param base_completed [Integer] Items completed before this phase
49
+ # @param total_items [Integer] Total items across all phases
50
+ # @return [Hash] Summary of actions taken
51
+ def run_with_progress(base_completed, total_items)
52
+ deduped = deduplicate_with_progress(base_completed, total_items)
53
+
54
+ # Update base for next sub-phase
55
+ dedup_count = @adapter.list_cards.select(&:embedding).size
56
+ conflicts = detect_conflicts_with_progress(base_completed + dedup_count, total_items)
57
+
58
+ # Update base for cluster phase
59
+ conflict_types = [:constraint, :decision, :fact]
60
+ conflict_count = @adapter.list_cards.count { |c| conflict_types.include?(c.type) && c.embedding }
61
+ clusters_updated = update_cluster_summaries_with_progress(
62
+ base_completed + dedup_count + conflict_count,
63
+ total_items,
64
+ )
65
+
66
+ {
67
+ duplicates_merged: deduped,
68
+ conflicts_detected: conflicts,
69
+ clusters_updated: clusters_updated,
70
+ }
71
+ end
72
+
73
+ # Find and merge duplicate cards
74
+ #
75
+ # @return [Integer] Number of duplicates merged
76
+ def deduplicate
77
+ cards = @adapter.list_cards
78
+ merged_count = 0
79
+ seen = Set.new
80
+
81
+ cards.each do |card|
82
+ next if seen.include?(card.id)
83
+ next unless card.embedding
84
+
85
+ # Find similar cards via vector search
86
+ similar = @adapter.vector_search(card.embedding, top_k: 5, threshold: @config.consolidator_dedup_threshold)
87
+
88
+ similar.each do |result|
89
+ next if result[:id] == card.id
90
+ next if seen.include?(result[:id])
91
+
92
+ duplicate = @adapter.read_card(result[:id])
93
+ next unless duplicate
94
+
95
+ merge_cards(canonical: card, duplicate: duplicate)
96
+ seen.add(duplicate.id)
97
+ merged_count += 1
98
+ end
99
+
100
+ seen.add(card.id)
101
+ end
102
+
103
+ merged_count
104
+ end
105
+
106
+ # Detect contradicting cards and create conflict edges
107
+ #
108
+ # Finds cards with high text similarity but different content,
109
+ # particularly when both are constraint or decision type cards.
110
+ # Creates `contradicts` edges between them.
111
+ #
112
+ # @return [Integer] Number of conflicts detected
113
+ def detect_conflicts
114
+ cards = @adapter.list_cards
115
+ conflict_types = [:constraint, :decision, :fact]
116
+ candidates = cards.select { |c| conflict_types.include?(c.type) && c.embedding }
117
+ conflict_count = 0
118
+ seen_pairs = Set.new
119
+
120
+ candidates.each do |card|
121
+ # Find similar cards that might contradict
122
+ similar = @adapter.vector_search(card.embedding, top_k: 5, threshold: @config.consolidator_conflict_threshold)
123
+
124
+ similar.each do |result|
125
+ next if result[:id] == card.id
126
+ next if result[:similarity] > @config.consolidator_dedup_threshold # Too similar = duplicate, not conflict
127
+
128
+ pair_key = [card.id, result[:id]].sort.join(":")
129
+ next if seen_pairs.include?(pair_key)
130
+
131
+ other = @adapter.read_card(result[:id])
132
+ next unless other
133
+ next unless conflict_types.include?(other.type)
134
+
135
+ # Check if an edge already exists between them
136
+ existing_edges = @adapter.edges_for(card.id)
137
+ other_id = result[:id]
138
+
139
+ already_contradicts = existing_edges.any? do |e|
140
+ (e.from_id == other_id || e.to_id == other_id) && e.type == :contradicts
141
+ end
142
+ next if already_contradicts
143
+
144
+ # Skip pairs connected by supporting edges — these reinforce
145
+ # the same fact in different phrasings, not contradictions
146
+ has_supporting_edge = existing_edges.any? do |e|
147
+ (e.from_id == other_id || e.to_id == other_id) && SUPPORTING_EDGE_TYPES.include?(e.type)
148
+ end
149
+ next if has_supporting_edge
150
+
151
+ # Cards are similar enough to be about the same topic but different
152
+ # enough to potentially contradict — flag them
153
+ edge = Edge.new(
154
+ from_id: card.id,
155
+ to_id: result[:id],
156
+ type: :contradicts,
157
+ weight: result[:similarity],
158
+ )
159
+ @adapter.write_edge(edge)
160
+ seen_pairs.add(pair_key)
161
+ conflict_count += 1
162
+ end
163
+ end
164
+
165
+ conflict_count
166
+ end
167
+
168
+ # Update rolling summaries for all clusters
169
+ #
170
+ # @return [Integer] Number of clusters updated
171
+ def update_cluster_summaries
172
+ clusters = @adapter.list_clusters
173
+ updated = 0
174
+
175
+ clusters.each do |cluster|
176
+ next if cluster.card_ids.empty?
177
+
178
+ # Load cluster cards
179
+ cards = cluster.card_ids.filter_map { |id| @adapter.read_card(id) }
180
+ next if cards.empty?
181
+
182
+ # Update key entities from member cards
183
+ all_entities = cards.flat_map(&:entities).tally
184
+ cluster.key_entities = all_entities.sort_by { |_, count| -count }.take(10).map(&:first)
185
+
186
+ # Update rolling summary from card texts
187
+ cluster.rolling_summary = cards.map(&:text).join(" | ").slice(0, 500)
188
+
189
+ # Update cluster embedding (average of member embeddings)
190
+ embeddings = cards.filter_map(&:embedding)
191
+ cluster.embedding = average_embedding(embeddings) unless embeddings.empty?
192
+
193
+ cluster.updated_at = Time.now
194
+ @adapter.write_cluster(cluster)
195
+ updated += 1
196
+ end
197
+
198
+ updated
199
+ end
200
+
201
+ # Find and merge duplicate cards with progress events
202
+ #
203
+ # @param base_completed [Integer] Items completed before this phase
204
+ # @param total_items [Integer] Total items across all phases
205
+ # @return [Integer] Number of duplicates merged
206
+ def deduplicate_with_progress(base_completed, total_items)
207
+ cards = @adapter.list_cards
208
+ cards_with_embedding = cards.select(&:embedding)
209
+ merged_count = 0
210
+ seen = Set.new
211
+
212
+ cards_with_embedding.each_with_index do |card, index|
213
+ unless seen.include?(card.id)
214
+ # Find similar cards via vector search
215
+ similar = @adapter.vector_search(card.embedding, top_k: 5, threshold: @config.consolidator_dedup_threshold)
216
+
217
+ similar.each do |result|
218
+ next if result[:id] == card.id
219
+ next if seen.include?(result[:id])
220
+
221
+ duplicate = @adapter.read_card(result[:id])
222
+ next unless duplicate
223
+
224
+ merge_cards(canonical: card, duplicate: duplicate)
225
+ seen.add(duplicate.id)
226
+ merged_count += 1
227
+ end
228
+
229
+ seen.add(card.id)
230
+ end
231
+
232
+ EventStream.emit(
233
+ type: "memory_defrag_progress",
234
+ phase: "consolidate_dedup",
235
+ description: "Finding and merging duplicate memory cards",
236
+ phase_current: index + 1,
237
+ phase_total: cards_with_embedding.size,
238
+ overall_current: base_completed + index + 1,
239
+ overall_total: total_items,
240
+ )
241
+ end
242
+
243
+ merged_count
244
+ end
245
+
246
+ # Detect contradicting cards with progress events
247
+ #
248
+ # @param base_completed [Integer] Items completed before this phase
249
+ # @param total_items [Integer] Total items across all phases
250
+ # @return [Integer] Number of conflicts detected
251
+ def detect_conflicts_with_progress(base_completed, total_items)
252
+ cards = @adapter.list_cards
253
+ conflict_types = [:constraint, :decision, :fact]
254
+ candidates = cards.select { |c| conflict_types.include?(c.type) && c.embedding }
255
+ conflict_count = 0
256
+ seen_pairs = Set.new
257
+
258
+ candidates.each_with_index do |card, index|
259
+ # Find similar cards that might contradict
260
+ similar = @adapter.vector_search(card.embedding, top_k: 5, threshold: @config.consolidator_conflict_threshold)
261
+
262
+ similar.each do |result|
263
+ next if result[:id] == card.id
264
+ next if result[:similarity] > @config.consolidator_dedup_threshold
265
+
266
+ pair_key = [card.id, result[:id]].sort.join(":")
267
+ next if seen_pairs.include?(pair_key)
268
+
269
+ other = @adapter.read_card(result[:id])
270
+ next unless other
271
+ next unless conflict_types.include?(other.type)
272
+
273
+ existing_edges = @adapter.edges_for(card.id)
274
+ other_id = result[:id]
275
+
276
+ already_contradicts = existing_edges.any? do |e|
277
+ (e.from_id == other_id || e.to_id == other_id) && e.type == :contradicts
278
+ end
279
+ next if already_contradicts
280
+
281
+ has_supporting_edge = existing_edges.any? do |e|
282
+ (e.from_id == other_id || e.to_id == other_id) && SUPPORTING_EDGE_TYPES.include?(e.type)
283
+ end
284
+ next if has_supporting_edge
285
+
286
+ edge = Edge.new(
287
+ from_id: card.id,
288
+ to_id: result[:id],
289
+ type: :contradicts,
290
+ weight: result[:similarity],
291
+ )
292
+ @adapter.write_edge(edge)
293
+ seen_pairs.add(pair_key)
294
+ conflict_count += 1
295
+ end
296
+
297
+ EventStream.emit(
298
+ type: "memory_defrag_progress",
299
+ phase: "consolidate_conflicts",
300
+ description: "Detecting contradicting information in memory",
301
+ phase_current: index + 1,
302
+ phase_total: candidates.size,
303
+ overall_current: base_completed + index + 1,
304
+ overall_total: total_items,
305
+ )
306
+ end
307
+
308
+ conflict_count
309
+ end
310
+
311
+ # Update cluster summaries with progress events
312
+ #
313
+ # @param base_completed [Integer] Items completed before this phase
314
+ # @param total_items [Integer] Total items across all phases
315
+ # @return [Integer] Number of clusters updated
316
+ def update_cluster_summaries_with_progress(base_completed, total_items)
317
+ clusters = @adapter.list_clusters
318
+ updated = 0
319
+
320
+ clusters.each_with_index do |cluster, index|
321
+ unless cluster.card_ids.empty?
322
+ cards = cluster.card_ids.filter_map { |id| @adapter.read_card(id) }
323
+ unless cards.empty?
324
+ all_entities = cards.flat_map(&:entities).tally
325
+ cluster.key_entities = all_entities.sort_by { |_, count| -count }.take(10).map(&:first)
326
+ cluster.rolling_summary = cards.map(&:text).join(" | ").slice(0, 500)
327
+ embeddings = cards.filter_map(&:embedding)
328
+ cluster.embedding = average_embedding(embeddings) unless embeddings.empty?
329
+ cluster.updated_at = Time.now
330
+ @adapter.write_cluster(cluster)
331
+ updated += 1
332
+ end
333
+ end
334
+
335
+ EventStream.emit(
336
+ type: "memory_defrag_progress",
337
+ phase: "consolidate_clusters",
338
+ description: "Updating cluster summaries and embeddings",
339
+ phase_current: index + 1,
340
+ phase_total: clusters.size,
341
+ overall_current: base_completed + index + 1,
342
+ overall_total: total_items,
343
+ )
344
+ end
345
+
346
+ updated
347
+ end
348
+
349
+ private
350
+
351
+ # Merge a duplicate card into a canonical card
352
+ #
353
+ # @param canonical [Card] Card to keep
354
+ # @param duplicate [Card] Card to merge and mark
355
+ # @return [void]
356
+ def merge_cards(canonical:, duplicate:)
357
+ # Transfer source turn IDs
358
+ combined_turns = (canonical.source_turn_ids + duplicate.source_turn_ids).uniq
359
+
360
+ # Transfer entities
361
+ combined_entities = (canonical.entities + duplicate.entities).uniq
362
+
363
+ # Keep the longer/more detailed text
364
+ if duplicate.text.length > canonical.text.length && duplicate.compression_level <= canonical.compression_level
365
+ canonical.text = duplicate.text
366
+ end
367
+
368
+ canonical.source_turn_ids = combined_turns
369
+ canonical.entities = combined_entities
370
+ canonical.access_count += duplicate.access_count
371
+ canonical.updated_at = Time.now
372
+
373
+ # Mark duplicate as merged
374
+ duplicate.canonical_id = canonical.id
375
+ duplicate.updated_at = Time.now
376
+
377
+ # Transfer edges from duplicate to canonical
378
+ edges = @adapter.edges_for(duplicate.id)
379
+ edges.each do |edge|
380
+ new_from = edge.from_id == duplicate.id ? canonical.id : edge.from_id
381
+ new_to = edge.to_id == duplicate.id ? canonical.id : edge.to_id
382
+ next if new_from == new_to # Skip self-edges
383
+
384
+ new_edge = Edge.new(
385
+ from_id: new_from,
386
+ to_id: new_to,
387
+ type: edge.type,
388
+ weight: edge.weight,
389
+ )
390
+ @adapter.write_edge(new_edge)
391
+ end
392
+
393
+ # Persist changes
394
+ @adapter.write_card(canonical)
395
+ @adapter.delete_card(duplicate.id)
396
+ @adapter.delete_edges_for(duplicate.id)
397
+
398
+ # Remove from clusters
399
+ @adapter.list_clusters.each do |cluster|
400
+ next unless cluster.card_ids.include?(duplicate.id)
401
+
402
+ cluster.remove_card(duplicate.id)
403
+ cluster.add_card(canonical.id) unless cluster.card_ids.include?(canonical.id)
404
+ @adapter.write_cluster(cluster)
405
+ end
406
+ end
407
+
408
+ # Average multiple embedding vectors
409
+ #
410
+ # @param embeddings [Array<Array<Float>>] Vectors to average
411
+ # @return [Array<Float>] Averaged vector
412
+ def average_embedding(embeddings)
413
+ return embeddings.first if embeddings.size == 1
414
+
415
+ dims = embeddings.first.size
416
+ avg = Array.new(dims, 0.0)
417
+
418
+ embeddings.each do |emb|
419
+ emb.each_with_index { |v, i| avg[i] += v }
420
+ end
421
+
422
+ avg.map { |v| v / embeddings.size }
423
+ end
424
+ end
425
+ end
426
+ end
427
+ end