claude_memory 0.7.1 → 0.9.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/rules/claude_memory.generated.md +32 -2
  4. data/.claude/settings.json +65 -15
  5. data/.claude/settings.local.json +5 -2
  6. data/.claude/skills/improve/SKILL.md +113 -25
  7. data/.claude/skills/upgrade-dependencies/SKILL.md +154 -0
  8. data/.claude-plugin/commands/distill-transcripts.md +98 -0
  9. data/.claude-plugin/commands/memory-recall.md +67 -0
  10. data/.claude-plugin/marketplace.json +2 -2
  11. data/.claude-plugin/plugin.json +3 -3
  12. data/.claude-plugin/scripts/hook-runner.sh +14 -0
  13. data/.claude-plugin/scripts/serve-mcp.sh +14 -0
  14. data/.ruby-version +1 -1
  15. data/CHANGELOG.md +90 -1
  16. data/CLAUDE.md +56 -18
  17. data/README.md +35 -0
  18. data/db/migrations/013_add_mcp_tool_calls.rb +26 -0
  19. data/db/migrations/014_canonicalize_predicates.rb +30 -0
  20. data/docs/improvements.md +74 -74
  21. data/docs/influence/claude-mem.md +1 -0
  22. data/docs/influence/claude-supermemory.md +1 -0
  23. data/docs/influence/episodic-memory.md +1 -0
  24. data/docs/influence/grepai.md +1 -0
  25. data/docs/influence/kbs.md +1 -0
  26. data/docs/influence/lossless-claw.md +1 -0
  27. data/docs/influence/qmd.md +1 -0
  28. data/docs/quality_review.md +119 -224
  29. data/hooks/hooks.json +39 -7
  30. data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
  31. data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
  32. data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
  33. data/lib/claude_memory/commands/completion_command.rb +149 -0
  34. data/lib/claude_memory/commands/doctor_command.rb +2 -0
  35. data/lib/claude_memory/commands/embeddings_command.rb +198 -0
  36. data/lib/claude_memory/commands/help_command.rb +12 -1
  37. data/lib/claude_memory/commands/hook_command.rb +2 -1
  38. data/lib/claude_memory/commands/index_command.rb +85 -78
  39. data/lib/claude_memory/commands/initializers/database_ensurer.rb +16 -0
  40. data/lib/claude_memory/commands/initializers/global_initializer.rb +2 -1
  41. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +55 -11
  42. data/lib/claude_memory/commands/initializers/project_initializer.rb +2 -1
  43. data/lib/claude_memory/commands/install_skill_command.rb +78 -0
  44. data/lib/claude_memory/commands/registry.rb +47 -32
  45. data/lib/claude_memory/commands/reject_command.rb +62 -0
  46. data/lib/claude_memory/commands/restore_command.rb +77 -0
  47. data/lib/claude_memory/commands/skills/distill-transcripts.md +102 -0
  48. data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
  49. data/lib/claude_memory/commands/stats_command.rb +98 -2
  50. data/lib/claude_memory/configuration.rb +14 -1
  51. data/lib/claude_memory/core/fact_ranker.rb +2 -2
  52. data/lib/claude_memory/core/rr_fusion.rb +23 -6
  53. data/lib/claude_memory/core/snippet_extractor.rb +7 -3
  54. data/lib/claude_memory/core/text_builder.rb +11 -0
  55. data/lib/claude_memory/distill/json_schema.md +8 -4
  56. data/lib/claude_memory/distill/null_distiller.rb +2 -0
  57. data/lib/claude_memory/domain/entity.rb +13 -1
  58. data/lib/claude_memory/domain/fact.rb +26 -2
  59. data/lib/claude_memory/domain/provenance.rb +0 -1
  60. data/lib/claude_memory/embeddings/api_adapter.rb +97 -0
  61. data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
  62. data/lib/claude_memory/embeddings/fastembed_adapter.rb +46 -12
  63. data/lib/claude_memory/embeddings/generator.rb +4 -0
  64. data/lib/claude_memory/embeddings/inspector.rb +91 -0
  65. data/lib/claude_memory/embeddings/model_registry.rb +210 -0
  66. data/lib/claude_memory/embeddings/resolver.rb +44 -0
  67. data/lib/claude_memory/hook/context_injector.rb +58 -2
  68. data/lib/claude_memory/hook/distillation_runner.rb +46 -0
  69. data/lib/claude_memory/hook/handler.rb +11 -2
  70. data/lib/claude_memory/index/vector_index.rb +15 -2
  71. data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
  72. data/lib/claude_memory/ingest/ingester.rb +17 -0
  73. data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
  74. data/lib/claude_memory/mcp/handlers/management_handlers.rb +169 -0
  75. data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
  76. data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
  77. data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
  78. data/lib/claude_memory/mcp/handlers/stats_handlers.rb +205 -0
  79. data/lib/claude_memory/mcp/instructions_builder.rb +19 -1
  80. data/lib/claude_memory/mcp/query_guide.rb +10 -0
  81. data/lib/claude_memory/mcp/response_formatter.rb +1 -0
  82. data/lib/claude_memory/mcp/server.rb +22 -1
  83. data/lib/claude_memory/mcp/telemetry.rb +86 -0
  84. data/lib/claude_memory/mcp/text_summary.rb +26 -0
  85. data/lib/claude_memory/mcp/tool_definitions.rb +116 -4
  86. data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
  87. data/lib/claude_memory/mcp/tools.rb +50 -679
  88. data/lib/claude_memory/publish.rb +40 -5
  89. data/lib/claude_memory/recall/dual_engine.rb +105 -0
  90. data/lib/claude_memory/recall/legacy_engine.rb +138 -0
  91. data/lib/claude_memory/recall/query_core.rb +371 -0
  92. data/lib/claude_memory/recall.rb +121 -673
  93. data/lib/claude_memory/resolve/predicate_policy.rb +63 -3
  94. data/lib/claude_memory/resolve/resolver.rb +43 -0
  95. data/lib/claude_memory/shortcuts.rb +4 -4
  96. data/lib/claude_memory/store/retry_handler.rb +61 -0
  97. data/lib/claude_memory/store/schema_manager.rb +68 -0
  98. data/lib/claude_memory/store/sqlite_store.rb +334 -201
  99. data/lib/claude_memory/store/store_manager.rb +50 -1
  100. data/lib/claude_memory/sweep/maintenance.rb +115 -1
  101. data/lib/claude_memory/sweep/sweeper.rb +3 -0
  102. data/lib/claude_memory/templates/hooks.example.json +26 -7
  103. data/lib/claude_memory/version.rb +1 -1
  104. data/lib/claude_memory.rb +16 -0
  105. metadata +48 -8
  106. data/.claude/memory.sqlite3-shm +0 -0
  107. data/.claude/memory.sqlite3-wal +0 -0
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module MCP
5
+ module Handlers
6
+ # Status and statistics tool handlers
7
+ module StatsHandlers
8
+ def status
9
+ result = {databases: {}}
10
+
11
+ if @manager
12
+ if @manager.global_exists?
13
+ @manager.ensure_global!
14
+ result[:databases][:global] = db_stats(@manager.global_store)
15
+ else
16
+ result[:databases][:global] = {exists: false}
17
+ end
18
+
19
+ if @manager.project_exists?
20
+ @manager.ensure_project!
21
+ result[:databases][:project] = db_stats(@manager.project_store)
22
+ else
23
+ result[:databases][:project] = {exists: false}
24
+ end
25
+ else
26
+ result[:databases][:legacy] = db_stats(@legacy_store)
27
+ end
28
+
29
+ result[:pending_distillation] = pending_distillation_count
30
+ result
31
+ end
32
+
33
+ def stats(args)
34
+ scope = args["scope"] || "all"
35
+ result = {scope: scope, databases: {}}
36
+
37
+ if @manager
38
+ if scope == "all" || scope == "global"
39
+ if @manager.global_exists?
40
+ @manager.ensure_global!
41
+ result[:databases][:global] = detailed_stats(@manager.global_store)
42
+ else
43
+ result[:databases][:global] = {exists: false}
44
+ end
45
+ end
46
+
47
+ if scope == "all" || scope == "project"
48
+ if @manager.project_exists?
49
+ @manager.ensure_project!
50
+ result[:databases][:project] = detailed_stats(@manager.project_store)
51
+ else
52
+ result[:databases][:project] = {exists: false}
53
+ end
54
+ end
55
+ else
56
+ result[:databases][:legacy] = detailed_stats(@legacy_store)
57
+ end
58
+
59
+ result
60
+ end
61
+
62
+ private
63
+
64
+ def pending_distillation_count
65
+ stores = if @manager
66
+ [@manager.global_exists? ? @manager.global_store : nil,
67
+ @manager.project_exists? ? @manager.project_store : nil].compact
68
+ elsif @legacy_store
69
+ [@legacy_store]
70
+ else
71
+ []
72
+ end
73
+
74
+ stores.sum { |store| store.count_undistilled(min_length: 200) }
75
+ end
76
+
77
+ def db_stats(store)
78
+ stats = {
79
+ exists: true,
80
+ facts_total: store.facts.count,
81
+ facts_active: store.facts.where(status: "active").count,
82
+ content_items: store.content_items.count,
83
+ open_conflicts: store.conflicts.where(status: "open").count,
84
+ schema_version: store.schema_version
85
+ }
86
+
87
+ vec_index = store.vector_index
88
+ stats[:vec_available] = vec_index.available?
89
+ stats[:vec_indexed] = vec_index.coverage_stats[:vec_indexed] if vec_index.available?
90
+
91
+ if fts_legacy?(store)
92
+ stats[:fts_legacy] = true
93
+ stats[:optimization_hint] = "Run 'claude-memory compact' to reduce database size by ~40%"
94
+ end
95
+
96
+ stats
97
+ end
98
+
99
+ def fts_legacy?(store)
100
+ row = store.db.fetch("SELECT sql FROM sqlite_master WHERE name = 'content_fts' AND type = 'table'").first
101
+ row && !row[:sql].to_s.include?("content=''")
102
+ rescue
103
+ false
104
+ end
105
+
106
+ def detailed_stats(store)
107
+ active_facts = store.facts.where(status: "active").count
108
+
109
+ stats = {
110
+ exists: true,
111
+ facts: fact_stats(store, active_facts),
112
+ entities: entity_stats(store),
113
+ content_items: content_stats(store),
114
+ provenance: provenance_stats(store, active_facts),
115
+ conflicts: conflict_stats(store),
116
+ schema_version: store.schema_version
117
+ }
118
+
119
+ stats[:vec] = vec_stats(store, active_facts)
120
+
121
+ stats
122
+ end
123
+
124
+ def fact_stats(store, active_facts)
125
+ stats = {
126
+ total: store.facts.count,
127
+ active: active_facts,
128
+ superseded: store.facts.where(status: "superseded").count
129
+ }
130
+
131
+ if active_facts > 0
132
+ all_predicates = store.db[:facts]
133
+ .where(status: "active")
134
+ .group_and_count(:predicate)
135
+ .order(Sequel.desc(:count))
136
+ .all
137
+ .map { |row| {predicate: row[:predicate], count: row[:count]} }
138
+
139
+ stats[:top_predicates] = all_predicates.first(10)
140
+ stats[:predicates_known], stats[:predicates_novel] =
141
+ all_predicates.partition { |row| Resolve::PredicatePolicy.known_predicates.include?(row[:predicate]) }
142
+ end
143
+
144
+ stats
145
+ end
146
+
147
+ def entity_stats(store)
148
+ {
149
+ total: store.entities.count,
150
+ by_type: store.db[:entities]
151
+ .group_and_count(:type)
152
+ .order(Sequel.desc(:count))
153
+ .all
154
+ .map { |row| {type: row[:type], count: row[:count]} }
155
+ }
156
+ end
157
+
158
+ def content_stats(store)
159
+ count = store.content_items.count
160
+ stats = {total: count}
161
+
162
+ if count > 0
163
+ stats[:date_range] = {
164
+ first: store.content_items.min(:occurred_at),
165
+ last: store.content_items.max(:occurred_at)
166
+ }
167
+ end
168
+
169
+ stats
170
+ end
171
+
172
+ def provenance_stats(store, active_facts)
173
+ return {facts_with_sources: 0, total_active_facts: 0, coverage_percentage: 0} if active_facts == 0
174
+
175
+ facts_with_provenance = store.db[:provenance]
176
+ .join(:facts, id: :fact_id)
177
+ .where(Sequel[:facts][:status] => "active")
178
+ .select(Sequel[:provenance][:fact_id])
179
+ .distinct
180
+ .count
181
+
182
+ {
183
+ facts_with_sources: facts_with_provenance,
184
+ total_active_facts: active_facts,
185
+ coverage_percentage: (facts_with_provenance * 100.0 / active_facts).round(1)
186
+ }
187
+ end
188
+
189
+ def vec_stats(store, _active_facts)
190
+ vec_index = store.vector_index
191
+ result = {available: vec_index.available?}
192
+ result.merge!(vec_index.coverage_stats) if vec_index.available?
193
+ result
194
+ end
195
+
196
+ def conflict_stats(store)
197
+ open = store.conflicts.where(status: "open").count
198
+ resolved = store.conflicts.where(status: "resolved").count
199
+
200
+ {open: open, resolved: resolved, total: open + resolved}
201
+ end
202
+ end
203
+ end
204
+ end
205
+ end
@@ -26,8 +26,9 @@ module ClaudeMemory
26
26
 
27
27
  parts << usage_hint(store_or_manager)
28
28
  parts.compact.join("\n\n")
29
- rescue => _e
29
+ rescue => e
30
30
  # Never fail initialization — return minimal instructions
31
+ ClaudeMemory.logger.debug("InstructionsBuilder failed: #{e.message}")
31
32
  "ClaudeMemory v#{ClaudeMemory::VERSION} — long-term memory for Claude Code."
32
33
  end
33
34
 
@@ -107,9 +108,26 @@ module ClaudeMemory
107
108
 
108
109
  escalation = vec ? "recall_semantic, explain, or fact_graph" : "explain or fact_graph"
109
110
  lines << "Start with fast tools (recall, decisions, conventions) before escalating to #{escalation}."
111
+
112
+ lines << proactive_recall_guidance
110
113
  lines.join("\n")
111
114
  end
112
115
 
116
+ # Directive guidance for when Claude should proactively consult memory.
117
+ # Validated by A/B testing: without these directives, Claude writes code
118
+ # using known-dangerous patterns (e.g. Sequel.sqlite) and hallucinates
119
+ # file paths instead of consulting memory for the correct structure.
120
+ def proactive_recall_guidance
121
+ <<~GUIDANCE.strip
122
+ IMPORTANT — check memory proactively in these situations:
123
+ - Before writing code: call memory.conventions to verify project patterns and avoid known gotchas
124
+ - Before explaining architecture: call memory.architecture for structural knowledge without file traversal
125
+ - Before refactoring: call memory.decisions to understand why past choices were made
126
+ - When asked about preferences: global facts store user environment and style preferences across all projects
127
+ - When adding to the codebase: recall which files and patterns to follow (memory knows correct paths and relationships)
128
+ GUIDANCE
129
+ end
130
+
113
131
  def count_by_predicates(store, predicates)
114
132
  store.facts
115
133
  .where(status: "active")
@@ -66,6 +66,16 @@ module ClaudeMemory
66
66
  - Use when: you need facts from a specific workflow context
67
67
  - Cost: ~300-800 tokens per call
68
68
 
69
+ ### Tier 5: Distillation Management
70
+
71
+ **memory.undistilled** — List content items not yet deeply distilled
72
+ - Use when: you want to find ingested content that hasn't been processed by LLM extraction
73
+ - Cost: ~200-400 tokens per call
74
+
75
+ **memory.mark_distilled** — Mark a content item as distilled after extraction
76
+ - Use after: performing LLM-based fact extraction on undistilled content
77
+ - Cost: ~100 tokens per call
78
+
69
79
  ## Recommended Workflow
70
80
 
71
81
  1. **Start broad**: `memory.recall` or shortcut tools (decisions/conventions/architecture)
@@ -278,6 +278,7 @@ module ClaudeMemory
278
278
  source: result[:source],
279
279
  similarity: result[:similarity]
280
280
  }
281
+ fact[:score_trace] = result[:score_trace] if result[:score_trace]
281
282
  fact[:receipts] = result[:receipts].map { |r| format_receipt(r, query: query) } unless compact
282
283
  fact
283
284
  end
@@ -5,20 +5,30 @@ require_relative "instructions_builder"
5
5
  require_relative "query_guide"
6
6
  require_relative "text_summary"
7
7
  require_relative "error_classifier"
8
+ require_relative "telemetry"
8
9
 
9
10
  module ClaudeMemory
10
11
  module MCP
12
+ # MCP JSON-RPC server over stdio.
13
+ # Reads newline-delimited JSON requests from input, dispatches to Tools,
14
+ # and writes JSON responses to output.
11
15
  class Server
12
16
  PROTOCOL_VERSION = "2024-11-05"
13
17
 
18
+ # @param store_or_manager [Store::SQLiteStore, Store::StoreManager] database backend
19
+ # @param input [IO] input stream for JSON-RPC requests (default: $stdin)
20
+ # @param output [IO] output stream for JSON-RPC responses (default: $stdout)
14
21
  def initialize(store_or_manager, input: $stdin, output: $stdout)
15
22
  @store_or_manager = store_or_manager
16
23
  @tools = Tools.new(store_or_manager)
24
+ @telemetry = Telemetry.new(store_or_manager)
17
25
  @input = input
18
26
  @output = output
19
27
  @running = false
20
28
  end
21
29
 
30
+ # Start the read loop, blocking until input is exhausted or stop is called.
31
+ # @return [void]
22
32
  def run
23
33
  @running = true
24
34
  while @running
@@ -29,12 +39,15 @@ module ClaudeMemory
29
39
  end
30
40
  end
31
41
 
42
+ # Signal the read loop to exit after the current message.
43
+ # @return [void]
32
44
  def stop
33
45
  @running = false
34
46
  end
35
47
 
36
48
  private
37
49
 
50
+ # @return [void]
38
51
  def handle_message(line)
39
52
  return if line.empty?
40
53
 
@@ -51,6 +64,7 @@ module ClaudeMemory
51
64
  end
52
65
  end
53
66
 
67
+ # @return [Hash, nil] JSON-RPC response hash, or nil for notifications
54
68
  def process_request(request)
55
69
  id = request["id"]
56
70
  method = request["method"]
@@ -74,6 +88,7 @@ module ClaudeMemory
74
88
  end
75
89
  end
76
90
 
91
+ # @return [Hash] initialize response with capabilities and server info
77
92
  def handle_initialize(id, _params)
78
93
  {
79
94
  jsonrpc: "2.0",
@@ -93,6 +108,7 @@ module ClaudeMemory
93
108
  }
94
109
  end
95
110
 
111
+ # @return [Hash] list of available tool definitions
96
112
  def handle_tools_list(id)
97
113
  {
98
114
  jsonrpc: "2.0",
@@ -103,11 +119,14 @@ module ClaudeMemory
103
119
  }
104
120
  end
105
121
 
122
+ # @return [Hash] tool result with dual content/structuredContent
106
123
  def handle_tools_call(id, params)
107
124
  name = params["name"]
108
125
  arguments = params["arguments"] || {}
109
126
 
110
- result = @tools.call(name, arguments)
127
+ result = @telemetry.record(name, arguments) do
128
+ @tools.call(name, arguments)
129
+ end
111
130
 
112
131
  # Release database connections after each tool call
113
132
  # This prevents lock contention with hook commands
@@ -128,6 +147,7 @@ module ClaudeMemory
128
147
  }
129
148
  end
130
149
 
150
+ # @return [Hash] list of available prompt definitions
131
151
  def handle_prompts_list(id)
132
152
  {
133
153
  jsonrpc: "2.0",
@@ -138,6 +158,7 @@ module ClaudeMemory
138
158
  }
139
159
  end
140
160
 
161
+ # @return [Hash] prompt content or error if unknown
141
162
  def handle_prompts_get(id, params)
142
163
  name = params&.dig("name")
143
164
 
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module MCP
5
+ # Records MCP tool invocations into the project database for usage stats.
6
+ # Timing and error capture wrap the tool call; the insert is synchronous
7
+ # and best-effort — telemetry failures are swallowed so they never break
8
+ # a real tool response.
9
+ class Telemetry
10
+ def initialize(store_or_manager)
11
+ @store_or_manager = store_or_manager
12
+ end
13
+
14
+ # Time a tool invocation and record the outcome. Yields to the caller
15
+ # and returns whatever the block returns; re-raises any exception after
16
+ # recording it as an error.
17
+ def record(tool_name, arguments)
18
+ started = monotonic_ms
19
+ begin
20
+ result = yield
21
+ rescue => e
22
+ duration = monotonic_ms - started
23
+ write(
24
+ tool_name: tool_name,
25
+ duration_ms: duration,
26
+ result_count: nil,
27
+ scope: extract_scope(arguments),
28
+ error_class: e.class.name
29
+ )
30
+ raise
31
+ end
32
+
33
+ duration = monotonic_ms - started
34
+ write(
35
+ tool_name: tool_name,
36
+ duration_ms: duration,
37
+ result_count: extract_result_count(result),
38
+ scope: extract_scope(arguments),
39
+ error_class: nil
40
+ )
41
+ result
42
+ end
43
+
44
+ private
45
+
46
+ def monotonic_ms
47
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i
48
+ end
49
+
50
+ def write(**row)
51
+ store = writable_store
52
+ return unless store
53
+ store.insert_mcp_tool_call(**row)
54
+ rescue Sequel::DatabaseError, Extralite::Error
55
+ # Telemetry is best-effort; never fail the user's tool call
56
+ # because stats couldn't be written.
57
+ end
58
+
59
+ def writable_store
60
+ if @store_or_manager.is_a?(Store::StoreManager)
61
+ @store_or_manager.ensure_project!
62
+ elsif @store_or_manager.respond_to?(:insert_mcp_tool_call)
63
+ @store_or_manager
64
+ end
65
+ end
66
+
67
+ def extract_scope(arguments)
68
+ return nil unless arguments.is_a?(Hash)
69
+ arguments["scope"] || arguments[:scope]
70
+ end
71
+
72
+ # Inspect a tool result for a countable field. Most query tools
73
+ # return hashes with :facts, :results, :conflicts, or :changes;
74
+ # fall back to nil for shapes we don't recognize.
75
+ def extract_result_count(result)
76
+ return nil unless result.is_a?(Hash)
77
+
78
+ %i[facts results conflicts changes entities items].each do |key|
79
+ value = result[key] || result[key.to_s]
80
+ return value.size if value.is_a?(Array)
81
+ end
82
+ nil
83
+ end
84
+ end
85
+ end
86
+ end
@@ -28,6 +28,8 @@ module ClaudeMemory
28
28
  when "memory.recall_semantic" then summarize_semantic(result)
29
29
  when "memory.search_concepts" then summarize_concepts(result)
30
30
  when "memory.fact_graph" then summarize_fact_graph(result)
31
+ when "memory.undistilled" then summarize_undistilled(result)
32
+ when "memory.mark_distilled" then summarize_mark_distilled(result)
31
33
  when "memory.check_setup" then summarize_check_setup(result)
32
34
  else JSON.generate(result)
33
35
  end
@@ -122,6 +124,10 @@ module ClaudeMemory
122
124
  "- #{name}: #{info[:facts_active]} active facts, #{info[:open_conflicts]} conflicts (schema v#{info[:schema_version]})"
123
125
  end
124
126
  end
127
+
128
+ pending = result[:pending_distillation] || 0
129
+ lines << "Pending distillation: #{pending}" if pending > 0
130
+
125
131
  lines.join("\n")
126
132
  end
127
133
 
@@ -233,6 +239,26 @@ module ClaudeMemory
233
239
  lines.join("\n")
234
240
  end
235
241
 
242
+ def self.summarize_undistilled(result)
243
+ items = result[:items] || []
244
+ return "No undistilled content items." if items.empty?
245
+
246
+ lines = ["#{result[:count]} undistilled content item(s):"]
247
+ items.each do |i|
248
+ ago = i[:occurred_ago] || "unknown"
249
+ lines << "- Item ##{i[:content_item_id]} (#{ago}): #{(i[:raw_text] || "")[0, 80]}..."
250
+ end
251
+ lines.join("\n")
252
+ end
253
+
254
+ def self.summarize_mark_distilled(result)
255
+ if result[:success]
256
+ "Marked content item ##{result[:content_item_id]} as distilled (#{result[:facts_extracted]} facts extracted)"
257
+ else
258
+ result[:error]
259
+ end
260
+ end
261
+
236
262
  def self.summarize_check_setup(result)
237
263
  lines = ["Setup status: #{result[:status]}"]
238
264
  lines << "Version: #{result[:version][:current]} (latest: #{result[:version][:latest]})"