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.
- checksums.yaml +4 -4
- data/.claude/memory.sqlite3 +0 -0
- data/.claude/rules/claude_memory.generated.md +42 -64
- data/.claude/skills/release/SKILL.md +44 -6
- data/.claude/skills/study-repo/SKILL.md +15 -0
- data/.claude-plugin/commands/audit-memory.md +68 -0
- data/.claude-plugin/marketplace.json +1 -1
- data/.claude-plugin/plugin.json +1 -1
- data/CHANGELOG.md +70 -0
- data/CLAUDE.md +20 -5
- data/README.md +64 -2
- data/db/migrations/018_add_otel_telemetry.rb +81 -0
- data/docs/1_0_punchlist.md +522 -89
- data/docs/GETTING_STARTED.md +3 -1
- data/docs/api_stability.md +341 -0
- data/docs/architecture.md +3 -3
- data/docs/audit_runbook.md +209 -0
- data/docs/claude_monitoring.md +956 -0
- data/docs/dashboard.md +23 -3
- data/docs/improvements.md +329 -5
- data/docs/influence/ai-memory-systems-2026.md +403 -0
- data/docs/memory_audit_2026-05-21.md +303 -0
- data/docs/plugin.md +1 -1
- data/docs/quality_review.md +35 -0
- data/lib/claude_memory/audit/checks.rb +239 -0
- data/lib/claude_memory/audit/finding.rb +33 -0
- data/lib/claude_memory/audit/runner.rb +73 -0
- data/lib/claude_memory/commands/audit_command.rb +117 -0
- data/lib/claude_memory/commands/dashboard_command.rb +2 -1
- data/lib/claude_memory/commands/digest_command.rb +95 -3
- data/lib/claude_memory/commands/hook_command.rb +27 -2
- data/lib/claude_memory/commands/import_auto_memory_command.rb +180 -0
- data/lib/claude_memory/commands/initializers/hooks_configurator.rb +7 -4
- data/lib/claude_memory/commands/otel_command.rb +240 -0
- data/lib/claude_memory/commands/registry.rb +5 -1
- data/lib/claude_memory/commands/show_command.rb +90 -0
- data/lib/claude_memory/commands/stats_command.rb +94 -2
- data/lib/claude_memory/configuration.rb +60 -0
- data/lib/claude_memory/core/fact_query_builder.rb +1 -0
- data/lib/claude_memory/dashboard/api.rb +8 -0
- data/lib/claude_memory/dashboard/index.html +140 -1
- data/lib/claude_memory/dashboard/prompt_journey.rb +48 -0
- data/lib/claude_memory/dashboard/server.rb +86 -0
- data/lib/claude_memory/dashboard/telemetry.rb +156 -0
- data/lib/claude_memory/dashboard/trust.rb +180 -11
- data/lib/claude_memory/deprecations.rb +106 -0
- data/lib/claude_memory/distill/bare_conclusion_detector.rb +71 -0
- data/lib/claude_memory/distill/reference_material_detector.rb +37 -4
- data/lib/claude_memory/hook/auto_memory_mirror.rb +7 -3
- data/lib/claude_memory/hook/context_injector.rb +11 -2
- data/lib/claude_memory/hook/handler.rb +142 -1
- data/lib/claude_memory/mcp/tool_definitions.rb +3 -3
- data/lib/claude_memory/otel/attributes.rb +118 -0
- data/lib/claude_memory/otel/constants.rb +32 -0
- data/lib/claude_memory/otel/ingestor.rb +54 -0
- data/lib/claude_memory/otel/otlp_json_envelope.rb +254 -0
- data/lib/claude_memory/otel/prompt_scope.rb +108 -0
- data/lib/claude_memory/otel/settings_writer.rb +122 -0
- data/lib/claude_memory/otel/status.rb +58 -0
- data/lib/claude_memory/recall/staleness_annotator.rb +73 -0
- data/lib/claude_memory/resolve/predicate_policy.rb +17 -1
- data/lib/claude_memory/resolve/resolver.rb +30 -3
- data/lib/claude_memory/shortcuts.rb +61 -18
- data/lib/claude_memory/store/prompt_journey_query.rb +87 -0
- data/lib/claude_memory/store/schema_manager.rb +1 -1
- data/lib/claude_memory/store/sqlite_store.rb +136 -0
- data/lib/claude_memory/sweep/maintenance.rb +31 -1
- data/lib/claude_memory/sweep/sweeper.rb +6 -0
- data/lib/claude_memory/templates/hooks.example.json +5 -0
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +20 -0
- 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")
|
|
124
|
-
# claim
|
|
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)
|
|
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
|
-
|
|
17
|
+
SHORTCUTS = {
|
|
6
18
|
decisions: {
|
|
7
|
-
|
|
8
|
-
scope: "all",
|
|
19
|
+
predicates: %w[decision],
|
|
9
20
|
limit: 10
|
|
10
21
|
},
|
|
11
22
|
architecture: {
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
18
|
-
scope: "global",
|
|
32
|
+
predicates: %w[convention],
|
|
19
33
|
limit: 20
|
|
20
34
|
},
|
|
21
35
|
project_config: {
|
|
22
|
-
|
|
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 =
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
@@ -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 }
|
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"
|