claude_memory 0.10.0 → 0.12.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/rules/claude_memory.generated.md +42 -64
  4. data/.claude/skills/release/SKILL.md +44 -6
  5. data/.claude/skills/study-repo/SKILL.md +15 -0
  6. data/.claude-plugin/commands/audit-memory.md +68 -0
  7. data/.claude-plugin/marketplace.json +1 -1
  8. data/.claude-plugin/plugin.json +1 -1
  9. data/CHANGELOG.md +70 -0
  10. data/CLAUDE.md +20 -5
  11. data/README.md +64 -2
  12. data/db/migrations/018_add_otel_telemetry.rb +81 -0
  13. data/docs/1_0_punchlist.md +522 -89
  14. data/docs/GETTING_STARTED.md +3 -1
  15. data/docs/api_stability.md +341 -0
  16. data/docs/architecture.md +3 -3
  17. data/docs/audit_runbook.md +209 -0
  18. data/docs/claude_monitoring.md +956 -0
  19. data/docs/dashboard.md +23 -3
  20. data/docs/improvements.md +329 -5
  21. data/docs/influence/ai-memory-systems-2026.md +403 -0
  22. data/docs/memory_audit_2026-05-21.md +303 -0
  23. data/docs/plugin.md +1 -1
  24. data/docs/quality_review.md +35 -0
  25. data/lib/claude_memory/audit/checks.rb +239 -0
  26. data/lib/claude_memory/audit/finding.rb +33 -0
  27. data/lib/claude_memory/audit/runner.rb +73 -0
  28. data/lib/claude_memory/commands/audit_command.rb +117 -0
  29. data/lib/claude_memory/commands/dashboard_command.rb +2 -1
  30. data/lib/claude_memory/commands/digest_command.rb +95 -3
  31. data/lib/claude_memory/commands/hook_command.rb +27 -2
  32. data/lib/claude_memory/commands/import_auto_memory_command.rb +180 -0
  33. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +7 -4
  34. data/lib/claude_memory/commands/otel_command.rb +240 -0
  35. data/lib/claude_memory/commands/registry.rb +5 -1
  36. data/lib/claude_memory/commands/show_command.rb +90 -0
  37. data/lib/claude_memory/commands/stats_command.rb +94 -2
  38. data/lib/claude_memory/configuration.rb +60 -0
  39. data/lib/claude_memory/core/fact_query_builder.rb +1 -0
  40. data/lib/claude_memory/dashboard/api.rb +8 -0
  41. data/lib/claude_memory/dashboard/index.html +140 -1
  42. data/lib/claude_memory/dashboard/prompt_journey.rb +48 -0
  43. data/lib/claude_memory/dashboard/server.rb +86 -0
  44. data/lib/claude_memory/dashboard/telemetry.rb +156 -0
  45. data/lib/claude_memory/dashboard/trust.rb +180 -11
  46. data/lib/claude_memory/deprecations.rb +106 -0
  47. data/lib/claude_memory/distill/bare_conclusion_detector.rb +71 -0
  48. data/lib/claude_memory/distill/reference_material_detector.rb +37 -4
  49. data/lib/claude_memory/hook/auto_memory_mirror.rb +7 -3
  50. data/lib/claude_memory/hook/context_injector.rb +11 -2
  51. data/lib/claude_memory/hook/handler.rb +142 -1
  52. data/lib/claude_memory/mcp/tool_definitions.rb +3 -3
  53. data/lib/claude_memory/otel/attributes.rb +118 -0
  54. data/lib/claude_memory/otel/constants.rb +32 -0
  55. data/lib/claude_memory/otel/ingestor.rb +54 -0
  56. data/lib/claude_memory/otel/otlp_json_envelope.rb +254 -0
  57. data/lib/claude_memory/otel/prompt_scope.rb +108 -0
  58. data/lib/claude_memory/otel/settings_writer.rb +122 -0
  59. data/lib/claude_memory/otel/status.rb +58 -0
  60. data/lib/claude_memory/recall/staleness_annotator.rb +73 -0
  61. data/lib/claude_memory/resolve/predicate_policy.rb +17 -1
  62. data/lib/claude_memory/resolve/resolver.rb +30 -3
  63. data/lib/claude_memory/shortcuts.rb +61 -18
  64. data/lib/claude_memory/store/prompt_journey_query.rb +87 -0
  65. data/lib/claude_memory/store/schema_manager.rb +1 -1
  66. data/lib/claude_memory/store/sqlite_store.rb +136 -0
  67. data/lib/claude_memory/sweep/maintenance.rb +31 -1
  68. data/lib/claude_memory/sweep/sweeper.rb +6 -0
  69. data/lib/claude_memory/templates/hooks.example.json +5 -0
  70. data/lib/claude_memory/version.rb +1 -1
  71. data/lib/claude_memory.rb +20 -0
  72. metadata +28 -1
@@ -62,9 +62,25 @@ module ClaudeMemory
62
62
 
63
63
  # Return the canonical form of a predicate name, applying known
64
64
  # synonym mappings. Leaves unmapped predicates unchanged.
65
+ #
66
+ # Emits a deprecation warning via `ClaudeMemory::Deprecations` when
67
+ # an actual synonym is hit, since the predicate vocabulary is part
68
+ # of the public API contract (`docs/api_stability.md` §6) and
69
+ # silent canonicalization makes the legacy form indistinguishable
70
+ # from the current one. Removal of the SYNONYMS entries is
71
+ # scheduled for `1.0.0`.
65
72
  def self.canonicalize(predicate)
66
73
  return predicate if predicate.nil?
67
- SYNONYMS.fetch(predicate, predicate)
74
+ canonical = SYNONYMS.fetch(predicate, predicate)
75
+ if canonical != predicate
76
+ ClaudeMemory::Deprecations.warn(
77
+ name: "predicate=#{predicate}",
78
+ replacement: "predicate=#{canonical}",
79
+ removed_in: "1.0.0",
80
+ message: "PredicatePolicy::SYNONYMS will be removed; emit canonical predicate names directly."
81
+ )
82
+ end
83
+ canonical
68
84
  end
69
85
 
70
86
  # Return the snapshot section a predicate belongs to.
@@ -120,15 +120,36 @@ module ClaudeMemory
120
120
 
121
121
  # No exact match: for multi-value predicates the new object is
122
122
  # genuinely a new coexisting value. For single-value, either the
123
- # user signaled supersession ("now we use X instead") or the new
124
- # claim contradicts the current one.
123
+ # user signaled supersession ("now we use X instead"), the new
124
+ # claim is example text (silently discarded), or the new claim
125
+ # contradicts the current one (conflict).
125
126
  if PredicatePolicy.single?(fact_data[:predicate])
126
- supersession_signal?(fact_data) ? :supersede : :conflict
127
+ return :supersede if supersession_signal?(fact_data)
128
+ return :discard if example_text_quote?(fact_data)
129
+ :conflict
127
130
  else
128
131
  :insert
129
132
  end
130
133
  end
131
134
 
135
+ # Single-cardinality stack predicates extracted from CLAUDE.md-style
136
+ # example text ('e.g., "this app uses PostgreSQL"') used to create a
137
+ # disputed fact + conflict row every ingest cycle. The
138
+ # ReferenceMaterialDetector handles this for the MCP
139
+ # `store_extraction` path; the resolver-side guard catches the same
140
+ # pattern when Layer-1 NullDistiller produces a stack fact from
141
+ # documentation text. Added 2026-05-21 audit Phase 3.6.
142
+ EXAMPLE_QUOTE_PATTERNS = [
143
+ /\b(?:e\.?g\.?|i\.?e\.?|for example|for instance|such as)[,:]?\s/i,
144
+ /\(\s*(?:e\.?g\.?|i\.?e\.?)[,.]/i
145
+ ].freeze
146
+
147
+ def example_text_quote?(fact_data)
148
+ quote = fact_data[:quote].to_s
149
+ return false if quote.empty?
150
+ EXAMPLE_QUOTE_PATTERNS.any? { |re| quote.match?(re) }
151
+ end
152
+
132
153
  def apply_resolution(resolution, fact_data, subject_id, entity_ids, content_item_id, occurred_at, existing_facts, project_path:, scope:)
133
154
  case resolution
134
155
  when :reinforce
@@ -136,6 +157,12 @@ module ClaudeMemory
136
157
  when :conflict
137
158
  apply_conflict(existing_facts, fact_data, subject_id, content_item_id, occurred_at,
138
159
  project_path: project_path, scope: scope)
160
+ when :discard
161
+ # Silently drop the fact — its quote looked like documentation
162
+ # example text against a single-cardinality predicate that
163
+ # already has a confirmed value. Returning zero across the
164
+ # board keeps the resolver-result accounting consistent.
165
+ {created: 0, superseded: 0, conflicts: 0, provenance: 0}
139
166
  else
140
167
  apply_insert(fact_data, subject_id, entity_ids, content_item_id, occurred_at, existing_facts, resolution,
141
168
  project_path: project_path, scope: scope)
@@ -1,40 +1,53 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeMemory
4
+ # Predicate-based shortcuts for the three common "give me X" queries that
5
+ # MCP clients (and humans via CLI) expect to be trivially fast and
6
+ # noise-free.
7
+ #
8
+ # Prior implementation did FTS text search ("convention style format
9
+ # pattern prefer") with a hardcoded global-only scope on `conventions`.
10
+ # That produced cross-predicate matches (uses_database rows leaking into
11
+ # the decisions shortcut) and silently dropped every project convention
12
+ # on the floor. Switching to predicate-based filtering at the store level
13
+ # eliminates both classes of bug; the cost is we no longer rank by FTS
14
+ # relevance, but for "list the project's conventions" that's the correct
15
+ # trade.
4
16
  class Shortcuts
5
- QUERIES = {
17
+ SHORTCUTS = {
6
18
  decisions: {
7
- query: "decision constraint rule requirement",
8
- scope: "all",
19
+ predicates: %w[decision],
9
20
  limit: 10
10
21
  },
11
22
  architecture: {
12
- query: "uses framework implements architecture pattern",
13
- scope: "all",
23
+ # Includes the stack-shaping predicates so an agent asking
24
+ # "what's the architecture?" gets both narrative architecture
25
+ # facts AND the constraints (uses_database, uses_framework, ...).
26
+ # Without these the shortcut returns only freeform architecture
27
+ # facts and the constraints section stays invisible.
28
+ predicates: %w[architecture uses_database uses_framework uses_language deployment_platform auth_method],
14
29
  limit: 10
15
30
  },
16
31
  conventions: {
17
- query: "convention style format pattern prefer",
18
- scope: "global",
32
+ predicates: %w[convention],
19
33
  limit: 20
20
34
  },
21
35
  project_config: {
22
- query: "uses requires depends_on configuration",
23
- scope: "project",
36
+ predicates: %w[uses_database uses_framework uses_language deployment_platform auth_method],
24
37
  limit: 10
25
38
  }
26
39
  }.freeze
27
40
 
41
+ # @param shortcut_name [Symbol] :decisions, :architecture, :conventions, or :project_config
42
+ # @param manager [Store::StoreManager] dual-database manager
43
+ # @param overrides [Hash] :limit override
44
+ # @return [Array<Hash>] result hashes with :fact, :receipts (empty), :source ("project"/"global")
28
45
  def self.for(shortcut_name, manager, **overrides)
29
- config = QUERIES.fetch(shortcut_name)
30
- options = config.merge(overrides)
31
-
32
- recall = Recall.new(manager)
33
- recall.query(
34
- options[:query],
35
- limit: options[:limit],
36
- scope: options[:scope]
37
- )
46
+ config = SHORTCUTS.fetch(shortcut_name)
47
+ limit = overrides[:limit] || config[:limit]
48
+ predicates = config[:predicates]
49
+
50
+ collect_facts(manager, predicates, limit)
38
51
  end
39
52
 
40
53
  def self.decisions(manager, **overrides)
@@ -52,5 +65,35 @@ module ClaudeMemory
52
65
  def self.project_config(manager, **overrides)
53
66
  self.for(:project_config, manager, **overrides)
54
67
  end
68
+
69
+ # Query both stores for active facts matching the given predicates.
70
+ # Project facts take precedence (returned first); global facts fill
71
+ # any remaining slots up to the limit. Does NOT create missing DBs —
72
+ # callers like activity_logging's "orphan manager" depend on this
73
+ # surface staying read-only when the project DB hasn't been
74
+ # initialized yet.
75
+ def self.collect_facts(manager, predicates, limit)
76
+ project_store = manager.store_if_exists("project")
77
+ global_store = manager.store_if_exists("global")
78
+
79
+ project_rows = fetch_active_facts(project_store, predicates, limit)
80
+ global_rows = fetch_active_facts(global_store, predicates, limit)
81
+
82
+ results = project_rows.map { |row| {fact: row, receipts: [], source: "project"} } +
83
+ global_rows.map { |row| {fact: row, receipts: [], source: "global"} }
84
+
85
+ results.first(limit)
86
+ end
87
+
88
+ def self.fetch_active_facts(store, predicates, limit)
89
+ return [] unless store
90
+
91
+ Core::FactQueryBuilder.build_facts_dataset(store)
92
+ .where(Sequel[:facts][:predicate] => predicates,
93
+ Sequel[:facts][:status] => "active")
94
+ .reverse_order(Sequel[:facts][:id])
95
+ .limit(limit)
96
+ .all
97
+ end
55
98
  end
56
99
  end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ClaudeMemory
4
+ module Store
5
+ # Cross-store query for the dashboard's Prompt Journey panel. OTel
6
+ # events live in the global DB (writes hit the receiver, which is
7
+ # process-wide); activity_events with a back-tagged prompt_id can
8
+ # live in either store (hooks fire per-project, so hook_ingest /
9
+ # hook_context rows land in the project DB, while global may carry
10
+ # cross-project events). The query reads from all available stores
11
+ # and orders the merged stream by occurred_at.
12
+ #
13
+ # Accepts either a single store (legacy callers) or a StoreManager.
14
+ # Returns plain row hashes shaped uniformly so the panel renders
15
+ # both sources without branching.
16
+ class PromptJourneyQuery
17
+ def initialize(store_or_manager)
18
+ @stores = if store_or_manager.respond_to?(:project_store) || store_or_manager.respond_to?(:global_store)
19
+ [store_or_manager.respond_to?(:project_store) ? store_or_manager.project_store : nil,
20
+ store_or_manager.respond_to?(:global_store) ? store_or_manager.global_store : nil].compact
21
+ else
22
+ [store_or_manager].compact
23
+ end
24
+ end
25
+
26
+ # @param prompt_id [String] OTel prompt.id UUID
27
+ # @return [Array<Hash>] rows ordered by occurred_at ascending
28
+ def fetch(prompt_id)
29
+ return [] if prompt_id.nil? || prompt_id.empty?
30
+
31
+ rows = @stores.flat_map { |store|
32
+ otel_rows(store, prompt_id) + activity_rows(store, prompt_id)
33
+ }
34
+ rows.sort_by { |r| r[:occurred_at].to_s }
35
+ end
36
+
37
+ private
38
+
39
+ def otel_rows(store, prompt_id)
40
+ return [] unless store&.db&.table_exists?(:otel_events)
41
+ store.otel_events
42
+ .where(prompt_id: prompt_id)
43
+ .order(:occurred_at)
44
+ .limit(500)
45
+ .all
46
+ .map { |row| present_otel(row) }
47
+ end
48
+
49
+ def activity_rows(store, prompt_id)
50
+ return [] unless store&.db&.table_exists?(:activity_events)
51
+ return [] unless store.activity_events.columns.include?(:prompt_id)
52
+ store.activity_events
53
+ .where(prompt_id: prompt_id)
54
+ .order(:occurred_at)
55
+ .limit(500)
56
+ .all
57
+ .map { |row| present_activity(row) }
58
+ end
59
+
60
+ def present_otel(row)
61
+ {
62
+ source: "otel",
63
+ id: row[:id],
64
+ name: row[:event_name],
65
+ session_id: row[:session_id],
66
+ prompt_id: row[:prompt_id],
67
+ occurred_at: row[:occurred_at],
68
+ attributes_json: row[:attributes_json]
69
+ }
70
+ end
71
+
72
+ def present_activity(row)
73
+ {
74
+ source: "activity",
75
+ id: row[:id],
76
+ name: row[:event_type],
77
+ session_id: row[:session_id],
78
+ prompt_id: row[:prompt_id],
79
+ occurred_at: row[:occurred_at],
80
+ status: row[:status],
81
+ duration_ms: row[:duration_ms],
82
+ detail_json: row[:detail_json]
83
+ }
84
+ end
85
+ end
86
+ end
87
+ end
@@ -5,7 +5,7 @@ module ClaudeMemory
5
5
  # Schema migration and version management for SQLiteStore.
6
6
  # Handles Sequel migrations, legacy version syncing, and initial setup.
7
7
  module SchemaManager
8
- SCHEMA_VERSION = 17
8
+ SCHEMA_VERSION = 18
9
9
 
10
10
  private
11
11
 
@@ -111,6 +111,15 @@ module ClaudeMemory
111
111
  # @return [Sequel::Dataset]
112
112
  def moment_feedback = @db[:moment_feedback]
113
113
 
114
+ # @return [Sequel::Dataset]
115
+ def otel_metrics = @db[:otel_metrics]
116
+
117
+ # @return [Sequel::Dataset]
118
+ def otel_events = @db[:otel_events]
119
+
120
+ # @return [Sequel::Dataset]
121
+ def otel_traces = @db[:otel_traces]
122
+
114
123
  # Upsert a thumbs-up/down verdict for a moment. One row per event_id
115
124
  # (unique constraint on the column) — repeat clicks overwrite. Returns
116
125
  # the persisted row.
@@ -170,6 +179,133 @@ module ClaudeMemory
170
179
  )
171
180
  end
172
181
 
182
+ # Insert one OTel metric data point. Two value columns let us preserve
183
+ # int64 precision for counters (token counts) without losing fidelity in
184
+ # Float — see migration 018.
185
+ #
186
+ # @param name [String] OTel metric name (e.g. "claude_code.token.usage")
187
+ # @param value_type [String] "int" or "double"
188
+ # @param value_int [Integer, nil] integer value when value_type == "int"
189
+ # @param value_float [Float, nil] float value when value_type == "double"
190
+ # @param unit [String, nil] OTel unit string ("tokens", "USD", "s", ...)
191
+ # @param attributes [Hash, nil] flattened attribute map
192
+ # @param resource [Hash, nil] resource attribute map
193
+ # @param recorded_at [String] ISO 8601 timestamp
194
+ # @return [Integer] inserted row id
195
+ def insert_otel_metric(name:, value_type:, recorded_at:, value_int: nil, value_float: nil,
196
+ unit: nil, attributes: nil, resource: nil)
197
+ otel_metrics.insert(otel_metric_row(
198
+ name: name, value_type: value_type, recorded_at: recorded_at,
199
+ value_int: value_int, value_float: value_float, unit: unit,
200
+ attributes: attributes, resource: resource
201
+ ))
202
+ end
203
+
204
+ # Bulk insert OTel metric rows in a single SQL statement. Hot-path
205
+ # callers (the OTLP receiver) batch dozens of points per request;
206
+ # multi_insert avoids the per-row prepare/bind overhead.
207
+ def bulk_insert_otel_metrics(rows)
208
+ return 0 if rows.empty?
209
+ otel_metrics.multi_insert(rows.map { |r| otel_metric_row(**r) })
210
+ rows.size
211
+ end
212
+
213
+ # Insert one OTel log-style event row.
214
+ #
215
+ # @param event_name [String] e.g. "user_prompt", "tool_result", "api_request"
216
+ # @param occurred_at [String] ISO 8601 timestamp
217
+ # @param session_id [String, nil]
218
+ # @param prompt_id [String, nil] UUID correlating events from one prompt
219
+ # @param attributes [Hash, nil]
220
+ # @param resource [Hash, nil]
221
+ # @return [Integer] inserted row id
222
+ def insert_otel_event(event_name:, occurred_at:, session_id: nil, prompt_id: nil,
223
+ attributes: nil, resource: nil)
224
+ otel_events.insert(otel_event_row(
225
+ event_name: event_name, occurred_at: occurred_at,
226
+ session_id: session_id, prompt_id: prompt_id,
227
+ attributes: attributes, resource: resource
228
+ ))
229
+ end
230
+
231
+ def bulk_insert_otel_events(rows)
232
+ return 0 if rows.empty?
233
+ otel_events.multi_insert(rows.map { |r| otel_event_row(**r) })
234
+ rows.size
235
+ end
236
+
237
+ # Insert one OTel trace span row. Only used when traces are explicitly
238
+ # opted in via Configuration#otel_traces_enabled?.
239
+ #
240
+ # @param trace_id [String]
241
+ # @param span_id [String]
242
+ # @param name [String]
243
+ # @param recorded_at [String]
244
+ # @param parent_span_id [String, nil]
245
+ # @param session_id [String, nil]
246
+ # @param prompt_id [String, nil]
247
+ # @param start_unix_nano [Integer, nil]
248
+ # @param end_unix_nano [Integer, nil]
249
+ # @param duration_ms [Integer, nil]
250
+ # @param status_code [String, nil]
251
+ # @param attributes [Hash, nil]
252
+ # @param resource [Hash, nil]
253
+ # @return [Integer] inserted row id
254
+ def insert_otel_trace_span(trace_id:, span_id:, name:, recorded_at:,
255
+ parent_span_id: nil, session_id: nil, prompt_id: nil,
256
+ start_unix_nano: nil, end_unix_nano: nil, duration_ms: nil,
257
+ status_code: nil, attributes: nil, resource: nil)
258
+ otel_traces.insert(otel_trace_row(
259
+ trace_id: trace_id, span_id: span_id, name: name, recorded_at: recorded_at,
260
+ parent_span_id: parent_span_id, session_id: session_id, prompt_id: prompt_id,
261
+ start_unix_nano: start_unix_nano, end_unix_nano: end_unix_nano,
262
+ duration_ms: duration_ms, status_code: status_code,
263
+ attributes: attributes, resource: resource
264
+ ))
265
+ end
266
+
267
+ def bulk_insert_otel_traces(rows)
268
+ return 0 if rows.empty?
269
+ otel_traces.multi_insert(rows.map { |r| otel_trace_row(**r) })
270
+ rows.size
271
+ end
272
+
273
+ private
274
+
275
+ def otel_metric_row(name:, value_type:, recorded_at:, value_int: nil, value_float: nil,
276
+ unit: nil, attributes: nil, resource: nil)
277
+ {
278
+ name: name, value_type: value_type, value_int: value_int, value_float: value_float,
279
+ unit: unit, attributes_json: attributes&.to_json, resource_json: resource&.to_json,
280
+ recorded_at: recorded_at
281
+ }
282
+ end
283
+
284
+ def otel_event_row(event_name:, occurred_at:, session_id: nil, prompt_id: nil,
285
+ attributes: nil, resource: nil)
286
+ {
287
+ event_name: event_name, session_id: session_id, prompt_id: prompt_id,
288
+ attributes_json: attributes&.to_json, resource_json: resource&.to_json,
289
+ occurred_at: occurred_at
290
+ }
291
+ end
292
+
293
+ def otel_trace_row(trace_id:, span_id:, name:, recorded_at:,
294
+ parent_span_id: nil, session_id: nil, prompt_id: nil,
295
+ start_unix_nano: nil, end_unix_nano: nil, duration_ms: nil,
296
+ status_code: nil, attributes: nil, resource: nil)
297
+ {
298
+ trace_id: trace_id, span_id: span_id, parent_span_id: parent_span_id,
299
+ name: name, session_id: session_id, prompt_id: prompt_id,
300
+ start_unix_nano: start_unix_nano, end_unix_nano: end_unix_nano,
301
+ duration_ms: duration_ms, status_code: status_code,
302
+ attributes_json: attributes&.to_json, resource_json: resource&.to_json,
303
+ recorded_at: recorded_at
304
+ }
305
+ end
306
+
307
+ public
308
+
173
309
  # --- Content items ---
174
310
 
175
311
  # Insert a content item or return the existing id if a duplicate
@@ -17,7 +17,10 @@ module ClaudeMemory
17
17
  proposed_fact_ttl_days: 14,
18
18
  disputed_fact_ttl_days: 30,
19
19
  content_retention_days: 30,
20
- mcp_tool_call_retention_days: 90
20
+ mcp_tool_call_retention_days: 90,
21
+ otel_metric_retention_days: 30,
22
+ otel_event_retention_days: 14,
23
+ otel_trace_retention_days: 7
21
24
  }.freeze
22
25
 
23
26
  attr_reader :store
@@ -249,6 +252,33 @@ module ClaudeMemory
249
252
  @store.mcp_tool_calls.where { called_at < cutoff }.delete
250
253
  end
251
254
 
255
+ # Delete OTel metric data points older than retention window.
256
+ # Returns: Integer count of deleted rows (0 if table missing).
257
+ def prune_old_otel_metrics
258
+ return 0 unless @store.db.table_exists?(:otel_metrics)
259
+
260
+ cutoff = cutoff_time(@config[:otel_metric_retention_days])
261
+ @store.otel_metrics.where { recorded_at < cutoff }.delete
262
+ end
263
+
264
+ # Delete OTel log-style events older than retention window.
265
+ # Returns: Integer count of deleted rows (0 if table missing).
266
+ def prune_old_otel_events
267
+ return 0 unless @store.db.table_exists?(:otel_events)
268
+
269
+ cutoff = cutoff_time(@config[:otel_event_retention_days])
270
+ @store.otel_events.where { occurred_at < cutoff }.delete
271
+ end
272
+
273
+ # Delete OTel trace spans older than retention window.
274
+ # Returns: Integer count of deleted rows (0 if table missing).
275
+ def prune_old_otel_traces
276
+ return 0 unless @store.db.table_exists?(:otel_traces)
277
+
278
+ cutoff = cutoff_time(@config[:otel_trace_retention_days])
279
+ @store.otel_traces.where { recorded_at < cutoff }.delete
280
+ end
281
+
252
282
  # Checkpoint the SQLite WAL file for compaction.
253
283
  # Returns: true
254
284
  def checkpoint_wal
@@ -8,6 +8,9 @@ module ClaudeMemory
8
8
  disputed_fact_ttl_days: 30,
9
9
  content_retention_days: 30,
10
10
  mcp_tool_call_retention_days: 90,
11
+ otel_metric_retention_days: 30,
12
+ otel_event_retention_days: 14,
13
+ otel_trace_retention_days: 7,
11
14
  default_budget_seconds: 5
12
15
  }.freeze
13
16
 
@@ -43,6 +46,9 @@ module ClaudeMemory
43
46
  run_if_within_budget { @stats[:orphaned_provenance_deleted] = maintenance.prune_orphaned_provenance }
44
47
  run_if_within_budget { @stats[:old_content_pruned] = maintenance.prune_old_content }
45
48
  run_if_within_budget { @stats[:mcp_tool_calls_pruned] = maintenance.prune_old_mcp_tool_calls }
49
+ run_if_within_budget { @stats[:otel_metrics_pruned] = maintenance.prune_old_otel_metrics }
50
+ run_if_within_budget { @stats[:otel_events_pruned] = maintenance.prune_old_otel_events }
51
+ run_if_within_budget { @stats[:otel_traces_pruned] = maintenance.prune_old_otel_traces }
46
52
  run_if_within_budget { @stats[:vec_backfilled] = maintenance.backfill_vec_index }
47
53
  run_if_within_budget { @stats[:vec_cleaned] = maintenance.cleanup_vec_expired }
48
54
  run_if_within_budget { @stats[:wal_checkpointed] = maintenance.checkpoint_wal }
@@ -68,6 +68,11 @@
68
68
  "command": "claude-memory hook sweep",
69
69
  "timeout": 30,
70
70
  "statusMessage": "Sweeping memory..."
71
+ },
72
+ {
73
+ "type": "command",
74
+ "command": "claude-memory hook nudge",
75
+ "timeout": 5
71
76
  }
72
77
  ]
73
78
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeMemory
4
- VERSION = "0.10.0"
4
+ VERSION = "0.12.0"
5
5
  end
data/lib/claude_memory.rb CHANGED
@@ -77,6 +77,14 @@ require_relative "claude_memory/commands/dedupe_conflicts_command"
77
77
  require_relative "claude_memory/commands/reclassify_references_command"
78
78
  require_relative "claude_memory/commands/census_command"
79
79
  require_relative "claude_memory/commands/dashboard_command"
80
+ require_relative "claude_memory/otel/constants"
81
+ require_relative "claude_memory/otel/attributes"
82
+ require_relative "claude_memory/otel/otlp_json_envelope"
83
+ require_relative "claude_memory/otel/ingestor"
84
+ require_relative "claude_memory/otel/settings_writer"
85
+ require_relative "claude_memory/otel/status"
86
+ require_relative "claude_memory/otel/prompt_scope"
87
+ require_relative "claude_memory/store/prompt_journey_query"
80
88
  require_relative "claude_memory/dashboard/fact_presenter"
81
89
  require_relative "claude_memory/dashboard/scoped_fact_resolver"
82
90
  require_relative "claude_memory/dashboard/conflicts"
@@ -87,16 +95,27 @@ require_relative "claude_memory/dashboard/knowledge"
87
95
  require_relative "claude_memory/dashboard/reuse"
88
96
  require_relative "claude_memory/dashboard/timeline"
89
97
  require_relative "claude_memory/dashboard/health"
98
+ require_relative "claude_memory/dashboard/telemetry"
99
+ require_relative "claude_memory/dashboard/prompt_journey"
90
100
  require_relative "claude_memory/dashboard/api"
91
101
  require_relative "claude_memory/dashboard/server"
92
102
  require_relative "claude_memory/commands/digest_command"
103
+ require_relative "claude_memory/commands/show_command"
104
+ require_relative "claude_memory/commands/otel_command"
105
+ require_relative "claude_memory/commands/import_auto_memory_command"
106
+ require_relative "claude_memory/audit/finding"
107
+ require_relative "claude_memory/audit/checks"
108
+ require_relative "claude_memory/audit/runner"
109
+ require_relative "claude_memory/commands/audit_command"
93
110
  require_relative "claude_memory/commands/registry"
94
111
  require_relative "claude_memory/cli"
95
112
  require_relative "claude_memory/configuration"
113
+ require_relative "claude_memory/deprecations"
96
114
  require_relative "claude_memory/distill/distiller"
97
115
  require_relative "claude_memory/distill/extraction"
98
116
  require_relative "claude_memory/distill/null_distiller"
99
117
  require_relative "claude_memory/distill/reference_material_detector"
118
+ require_relative "claude_memory/distill/bare_conclusion_detector"
100
119
  require_relative "claude_memory/domain/fact"
101
120
  require_relative "claude_memory/domain/entity"
102
121
  require_relative "claude_memory/domain/provenance"
@@ -147,6 +166,7 @@ require_relative "claude_memory/recall/query_core"
147
166
  require_relative "claude_memory/recall/legacy_engine"
148
167
  require_relative "claude_memory/recall/dual_engine"
149
168
  require_relative "claude_memory/recall/stale_detector"
169
+ require_relative "claude_memory/recall/staleness_annotator"
150
170
  require_relative "claude_memory/recall"
151
171
  require_relative "claude_memory/shortcuts"
152
172
  require_relative "claude_memory/resolve/predicate_policy"