claude_memory 0.2.0 → 0.3.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/.mind.mv2.o2N83S +0 -0
- data/.claude/CLAUDE.md +1 -0
- data/.claude/rules/claude_memory.generated.md +28 -9
- data/.claude/settings.local.json +9 -1
- data/.claude/skills/check-memory/SKILL.md +77 -0
- data/.claude/skills/improve/SKILL.md +532 -0
- data/.claude/skills/improve/feature-patterns.md +1221 -0
- data/.claude/skills/quality-update/SKILL.md +229 -0
- data/.claude/skills/quality-update/implementation-guide.md +346 -0
- data/.claude/skills/review-commit/SKILL.md +199 -0
- data/.claude/skills/review-for-quality/SKILL.md +154 -0
- data/.claude/skills/review-for-quality/expert-checklists.md +79 -0
- data/.claude/skills/setup-memory/SKILL.md +168 -0
- data/.claude/skills/study-repo/SKILL.md +307 -0
- data/.claude/skills/study-repo/analysis-template.md +323 -0
- data/.claude/skills/study-repo/focus-examples.md +327 -0
- data/CHANGELOG.md +133 -0
- data/CLAUDE.md +130 -11
- data/README.md +117 -10
- data/db/migrations/001_create_initial_schema.rb +117 -0
- data/db/migrations/002_add_project_scoping.rb +33 -0
- data/db/migrations/003_add_session_metadata.rb +42 -0
- data/db/migrations/004_add_fact_embeddings.rb +20 -0
- data/db/migrations/005_add_incremental_sync.rb +21 -0
- data/db/migrations/006_add_operation_tracking.rb +40 -0
- data/db/migrations/007_add_ingestion_metrics.rb +26 -0
- data/docs/.claude/mind.mv2.lock +0 -0
- data/docs/GETTING_STARTED.md +587 -0
- data/docs/RELEASE_NOTES_v0.2.0.md +0 -1
- data/docs/RUBY_COMMUNITY_POST_v0.2.0.md +0 -2
- data/docs/architecture.md +9 -8
- data/docs/auto_init_design.md +230 -0
- data/docs/improvements.md +557 -731
- data/docs/influence/.gitkeep +13 -0
- data/docs/influence/grepai.md +933 -0
- data/docs/influence/qmd.md +2195 -0
- data/docs/plugin.md +257 -11
- data/docs/quality_review.md +472 -1273
- data/docs/remaining_improvements.md +330 -0
- data/lefthook.yml +13 -0
- data/lib/claude_memory/commands/checks/claude_md_check.rb +41 -0
- data/lib/claude_memory/commands/checks/database_check.rb +120 -0
- data/lib/claude_memory/commands/checks/hooks_check.rb +112 -0
- data/lib/claude_memory/commands/checks/reporter.rb +110 -0
- data/lib/claude_memory/commands/checks/snapshot_check.rb +30 -0
- data/lib/claude_memory/commands/doctor_command.rb +12 -129
- data/lib/claude_memory/commands/help_command.rb +1 -0
- data/lib/claude_memory/commands/hook_command.rb +9 -2
- data/lib/claude_memory/commands/index_command.rb +169 -0
- data/lib/claude_memory/commands/ingest_command.rb +1 -1
- data/lib/claude_memory/commands/init_command.rb +5 -197
- data/lib/claude_memory/commands/initializers/database_ensurer.rb +30 -0
- data/lib/claude_memory/commands/initializers/global_initializer.rb +85 -0
- data/lib/claude_memory/commands/initializers/hooks_configurator.rb +156 -0
- data/lib/claude_memory/commands/initializers/mcp_configurator.rb +56 -0
- data/lib/claude_memory/commands/initializers/memory_instructions_writer.rb +135 -0
- data/lib/claude_memory/commands/initializers/project_initializer.rb +111 -0
- data/lib/claude_memory/commands/recover_command.rb +75 -0
- data/lib/claude_memory/commands/registry.rb +5 -1
- data/lib/claude_memory/commands/stats_command.rb +239 -0
- data/lib/claude_memory/commands/uninstall_command.rb +226 -0
- data/lib/claude_memory/core/batch_loader.rb +32 -0
- data/lib/claude_memory/core/concept_ranker.rb +73 -0
- data/lib/claude_memory/core/embedding_candidate_builder.rb +37 -0
- data/lib/claude_memory/core/fact_collector.rb +51 -0
- data/lib/claude_memory/core/fact_query_builder.rb +154 -0
- data/lib/claude_memory/core/fact_ranker.rb +113 -0
- data/lib/claude_memory/core/result_builder.rb +54 -0
- data/lib/claude_memory/core/result_sorter.rb +25 -0
- data/lib/claude_memory/core/scope_filter.rb +61 -0
- data/lib/claude_memory/core/text_builder.rb +29 -0
- data/lib/claude_memory/embeddings/generator.rb +161 -0
- data/lib/claude_memory/embeddings/similarity.rb +69 -0
- data/lib/claude_memory/hook/handler.rb +4 -3
- data/lib/claude_memory/index/lexical_fts.rb +7 -2
- data/lib/claude_memory/infrastructure/operation_tracker.rb +158 -0
- data/lib/claude_memory/infrastructure/schema_validator.rb +206 -0
- data/lib/claude_memory/ingest/content_sanitizer.rb +6 -7
- data/lib/claude_memory/ingest/ingester.rb +99 -15
- data/lib/claude_memory/ingest/metadata_extractor.rb +57 -0
- data/lib/claude_memory/ingest/tool_extractor.rb +71 -0
- data/lib/claude_memory/mcp/response_formatter.rb +331 -0
- data/lib/claude_memory/mcp/server.rb +19 -0
- data/lib/claude_memory/mcp/setup_status_analyzer.rb +73 -0
- data/lib/claude_memory/mcp/tool_definitions.rb +279 -0
- data/lib/claude_memory/mcp/tool_helpers.rb +80 -0
- data/lib/claude_memory/mcp/tools.rb +330 -320
- data/lib/claude_memory/recall/dual_query_template.rb +63 -0
- data/lib/claude_memory/recall.rb +304 -237
- data/lib/claude_memory/resolve/resolver.rb +52 -49
- data/lib/claude_memory/store/sqlite_store.rb +210 -144
- data/lib/claude_memory/store/store_manager.rb +6 -6
- data/lib/claude_memory/sweep/sweeper.rb +6 -0
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +35 -3
- metadata +71 -11
- data/.claude/.mind.mv2.aLCUZd +0 -0
- data/.claude/memory.sqlite3 +0 -0
- data/.mcp.json +0 -11
- /data/docs/{feature_adoption_plan.md → plans/feature_adoption_plan.md} +0 -0
- /data/docs/{feature_adoption_plan_revised.md → plans/feature_adoption_plan_revised.md} +0 -0
- /data/docs/{plan.md → plans/plan.md} +0 -0
- /data/docs/{updated_plan.md → plans/updated_plan.md} +0 -0
|
@@ -20,15 +20,19 @@ module ClaudeMemory
|
|
|
20
20
|
provenance_created: 0
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
23
|
+
# Wrap entire extraction in a single transaction for better concurrency
|
|
24
|
+
# This reduces database lock time compared to per-fact transactions
|
|
25
|
+
@store.db.transaction do
|
|
26
|
+
entity_ids = resolve_entities(extraction.entities)
|
|
27
|
+
result[:entities_created] = entity_ids.size
|
|
28
|
+
|
|
29
|
+
extraction.facts.each do |fact_data|
|
|
30
|
+
outcome = resolve_fact(fact_data, entity_ids, content_item_id, occurred_at)
|
|
31
|
+
result[:facts_created] += outcome[:created]
|
|
32
|
+
result[:facts_superseded] += outcome[:superseded]
|
|
33
|
+
result[:conflicts_created] += outcome[:conflicts]
|
|
34
|
+
result[:provenance_created] += outcome[:provenance]
|
|
35
|
+
end
|
|
32
36
|
end
|
|
33
37
|
|
|
34
38
|
result
|
|
@@ -57,51 +61,50 @@ module ClaudeMemory
|
|
|
57
61
|
|
|
58
62
|
existing_facts = @store.facts_for_slot(subject_id, predicate)
|
|
59
63
|
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
end
|
|
64
|
+
# No transaction wrapper needed - handled by apply method
|
|
65
|
+
# This allows all facts to be processed in a single transaction
|
|
66
|
+
if PredicatePolicy.single?(predicate) && existing_facts.any?
|
|
67
|
+
matching = existing_facts.find { |f| values_match?(f, object_val, object_entity_id) }
|
|
68
|
+
if matching
|
|
69
|
+
add_provenance(matching[:id], content_item_id, fact_data)
|
|
70
|
+
outcome[:provenance] = 1
|
|
71
|
+
return outcome
|
|
72
|
+
elsif supersession_signal?(fact_data)
|
|
73
|
+
supersede_facts(existing_facts, occurred_at)
|
|
74
|
+
outcome[:superseded] = existing_facts.size
|
|
75
|
+
else
|
|
76
|
+
create_conflict(existing_facts.first[:id], fact_data, subject_id, content_item_id, occurred_at)
|
|
77
|
+
outcome[:conflicts] = 1
|
|
78
|
+
return outcome
|
|
76
79
|
end
|
|
80
|
+
end
|
|
77
81
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
fact_id = @store.insert_fact(
|
|
82
|
-
subject_entity_id: subject_id,
|
|
83
|
-
predicate: predicate,
|
|
84
|
-
object_entity_id: object_entity_id,
|
|
85
|
-
object_literal: object_val,
|
|
86
|
-
polarity: fact_data[:polarity] || "positive",
|
|
87
|
-
confidence: fact_data[:confidence] || 1.0,
|
|
88
|
-
valid_from: occurred_at,
|
|
89
|
-
scope: fact_scope,
|
|
90
|
-
project_path: fact_project
|
|
91
|
-
)
|
|
92
|
-
outcome[:created] = 1
|
|
93
|
-
|
|
94
|
-
if existing_facts.any? && outcome[:superseded] > 0
|
|
95
|
-
existing_facts.each do |old_fact|
|
|
96
|
-
@store.insert_fact_link(from_fact_id: fact_id, to_fact_id: old_fact[:id], link_type: "supersedes")
|
|
97
|
-
end
|
|
98
|
-
end
|
|
82
|
+
fact_scope = fact_data[:scope_hint] || @current_scope
|
|
83
|
+
fact_project = (fact_scope == "global") ? nil : @current_project_path
|
|
99
84
|
|
|
100
|
-
|
|
101
|
-
|
|
85
|
+
fact_id = @store.insert_fact(
|
|
86
|
+
subject_entity_id: subject_id,
|
|
87
|
+
predicate: predicate,
|
|
88
|
+
object_entity_id: object_entity_id,
|
|
89
|
+
object_literal: object_val,
|
|
90
|
+
polarity: fact_data[:polarity] || "positive",
|
|
91
|
+
confidence: fact_data[:confidence] || 1.0,
|
|
92
|
+
valid_from: occurred_at,
|
|
93
|
+
scope: fact_scope,
|
|
94
|
+
project_path: fact_project
|
|
95
|
+
)
|
|
96
|
+
outcome[:created] = 1
|
|
102
97
|
|
|
103
|
-
|
|
98
|
+
if existing_facts.any? && outcome[:superseded] > 0
|
|
99
|
+
existing_facts.each do |old_fact|
|
|
100
|
+
@store.insert_fact_link(from_fact_id: fact_id, to_fact_id: old_fact[:id], link_type: "supersedes")
|
|
101
|
+
end
|
|
104
102
|
end
|
|
103
|
+
|
|
104
|
+
add_provenance(fact_id, content_item_id, fact_data)
|
|
105
|
+
outcome[:provenance] = 1
|
|
106
|
+
|
|
107
|
+
outcome
|
|
105
108
|
end
|
|
106
109
|
|
|
107
110
|
def supersession_signal?(fact_data)
|
|
@@ -1,25 +1,63 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "sequel"
|
|
4
|
+
require "sequel/extensions/migration"
|
|
4
5
|
require "json"
|
|
6
|
+
require "extralite"
|
|
7
|
+
require "sequel/adapters/extralite"
|
|
5
8
|
|
|
6
9
|
module ClaudeMemory
|
|
7
10
|
module Store
|
|
8
11
|
class SQLiteStore
|
|
9
|
-
SCHEMA_VERSION =
|
|
12
|
+
SCHEMA_VERSION = 7
|
|
10
13
|
|
|
11
14
|
attr_reader :db
|
|
12
15
|
|
|
13
16
|
def initialize(db_path)
|
|
14
17
|
@db_path = db_path
|
|
15
|
-
@db =
|
|
18
|
+
@db = connect_database(db_path)
|
|
19
|
+
|
|
20
|
+
configure_pragmas
|
|
21
|
+
|
|
16
22
|
ensure_schema!
|
|
17
23
|
end
|
|
18
24
|
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def connect_database(db_path)
|
|
28
|
+
# Extralite adapter: Better performance and GVL release
|
|
29
|
+
Sequel.connect("extralite:#{db_path}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def configure_pragmas
|
|
33
|
+
# Enable WAL mode for better concurrency
|
|
34
|
+
# - Multiple readers don't block each other
|
|
35
|
+
# - Writers don't block readers
|
|
36
|
+
# - Safer concurrent hook execution
|
|
37
|
+
@db.run("PRAGMA journal_mode = WAL")
|
|
38
|
+
@db.run("PRAGMA synchronous = NORMAL")
|
|
39
|
+
|
|
40
|
+
# Set busy timeout to 30 seconds (increased from 5s)
|
|
41
|
+
# - Allows much longer wait times before raising BusyException
|
|
42
|
+
# - Critical for concurrent hook execution with MCP server
|
|
43
|
+
# - Combined with ingester retry logic, provides ~5 minutes total wait
|
|
44
|
+
# - Extralite releases GVL for better threading performance
|
|
45
|
+
@db.run("PRAGMA busy_timeout = 30000")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
public
|
|
49
|
+
|
|
19
50
|
def close
|
|
20
51
|
@db.disconnect
|
|
21
52
|
end
|
|
22
53
|
|
|
54
|
+
# Checkpoint the WAL file to prevent unlimited growth
|
|
55
|
+
# This truncates the WAL after checkpointing
|
|
56
|
+
# Should be called periodically during maintenance/sweep operations
|
|
57
|
+
def checkpoint_wal
|
|
58
|
+
@db.run("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
59
|
+
end
|
|
60
|
+
|
|
23
61
|
def schema_version
|
|
24
62
|
@db[:meta].where(key: "schema_version").get(:value)&.to_i
|
|
25
63
|
end
|
|
@@ -56,155 +94,25 @@ module ClaudeMemory
|
|
|
56
94
|
@db[:conflicts]
|
|
57
95
|
end
|
|
58
96
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def ensure_schema!
|
|
62
|
-
create_tables!
|
|
63
|
-
run_migrations!
|
|
64
|
-
set_meta("schema_version", SCHEMA_VERSION.to_s)
|
|
65
|
-
set_meta("created_at", Time.now.utc.iso8601) unless get_meta("created_at")
|
|
97
|
+
def tool_calls
|
|
98
|
+
@db[:tool_calls]
|
|
66
99
|
end
|
|
67
100
|
|
|
68
|
-
def
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
migrate_to_v2! if current < 2
|
|
101
|
+
def operation_progress
|
|
102
|
+
@db[:operation_progress]
|
|
72
103
|
end
|
|
73
104
|
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
unless columns.include?(:project_path)
|
|
77
|
-
@db.alter_table(:content_items) do
|
|
78
|
-
add_column :project_path, String
|
|
79
|
-
end
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
columns = @db.schema(:facts).map(&:first)
|
|
83
|
-
unless columns.include?(:scope)
|
|
84
|
-
@db.alter_table(:facts) do
|
|
85
|
-
add_column :scope, String, default: "project"
|
|
86
|
-
add_column :project_path, String
|
|
87
|
-
add_index :scope, name: :idx_facts_scope
|
|
88
|
-
add_index :project_path, name: :idx_facts_project
|
|
89
|
-
end
|
|
90
|
-
end
|
|
105
|
+
def schema_health
|
|
106
|
+
@db[:schema_health]
|
|
91
107
|
end
|
|
92
108
|
|
|
93
|
-
def
|
|
94
|
-
@db
|
|
95
|
-
String :key, primary_key: true
|
|
96
|
-
String :value
|
|
97
|
-
end
|
|
98
|
-
|
|
99
|
-
@db.create_table?(:content_items) do
|
|
100
|
-
primary_key :id
|
|
101
|
-
String :source, null: false
|
|
102
|
-
String :session_id
|
|
103
|
-
String :transcript_path
|
|
104
|
-
String :project_path
|
|
105
|
-
String :occurred_at
|
|
106
|
-
String :ingested_at, null: false
|
|
107
|
-
String :text_hash, null: false
|
|
108
|
-
Integer :byte_len, null: false
|
|
109
|
-
String :raw_text, text: true
|
|
110
|
-
String :metadata_json, text: true
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
@db.create_table?(:delta_cursors) do
|
|
114
|
-
primary_key :id
|
|
115
|
-
String :session_id, null: false
|
|
116
|
-
String :transcript_path, null: false
|
|
117
|
-
Integer :last_byte_offset, null: false, default: 0
|
|
118
|
-
String :updated_at, null: false
|
|
119
|
-
unique [:session_id, :transcript_path]
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
@db.create_table?(:entities) do
|
|
123
|
-
primary_key :id
|
|
124
|
-
String :type, null: false
|
|
125
|
-
String :canonical_name, null: false
|
|
126
|
-
String :slug, null: false, unique: true
|
|
127
|
-
String :created_at, null: false
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
@db.create_table?(:entity_aliases) do
|
|
131
|
-
primary_key :id
|
|
132
|
-
foreign_key :entity_id, :entities, null: false
|
|
133
|
-
String :source
|
|
134
|
-
String :alias, null: false
|
|
135
|
-
Float :confidence, default: 1.0
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
@db.create_table?(:facts) do
|
|
139
|
-
primary_key :id
|
|
140
|
-
foreign_key :subject_entity_id, :entities
|
|
141
|
-
String :predicate, null: false
|
|
142
|
-
foreign_key :object_entity_id, :entities
|
|
143
|
-
String :object_literal
|
|
144
|
-
String :datatype
|
|
145
|
-
String :polarity, default: "positive"
|
|
146
|
-
String :valid_from
|
|
147
|
-
String :valid_to
|
|
148
|
-
String :status, default: "active"
|
|
149
|
-
Float :confidence, default: 1.0
|
|
150
|
-
String :created_from
|
|
151
|
-
String :created_at, null: false
|
|
152
|
-
String :scope, default: "project"
|
|
153
|
-
String :project_path
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
@db.create_table?(:provenance) do
|
|
157
|
-
primary_key :id
|
|
158
|
-
foreign_key :fact_id, :facts, null: false
|
|
159
|
-
foreign_key :content_item_id, :content_items
|
|
160
|
-
String :quote, text: true
|
|
161
|
-
foreign_key :attribution_entity_id, :entities
|
|
162
|
-
String :strength, default: "stated"
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
@db.create_table?(:fact_links) do
|
|
166
|
-
primary_key :id
|
|
167
|
-
foreign_key :from_fact_id, :facts, null: false
|
|
168
|
-
foreign_key :to_fact_id, :facts, null: false
|
|
169
|
-
String :link_type, null: false
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
@db.create_table?(:conflicts) do
|
|
173
|
-
primary_key :id
|
|
174
|
-
foreign_key :fact_a_id, :facts, null: false
|
|
175
|
-
foreign_key :fact_b_id, :facts, null: false
|
|
176
|
-
String :status, default: "open"
|
|
177
|
-
String :detected_at, null: false
|
|
178
|
-
String :notes, text: true
|
|
179
|
-
end
|
|
180
|
-
|
|
181
|
-
create_index_if_not_exists(:facts, :predicate, :idx_facts_predicate)
|
|
182
|
-
create_index_if_not_exists(:facts, :subject_entity_id, :idx_facts_subject)
|
|
183
|
-
create_index_if_not_exists(:facts, :status, :idx_facts_status)
|
|
184
|
-
create_index_if_not_exists(:facts, :scope, :idx_facts_scope)
|
|
185
|
-
create_index_if_not_exists(:facts, :project_path, :idx_facts_project)
|
|
186
|
-
create_index_if_not_exists(:provenance, :fact_id, :idx_provenance_fact)
|
|
187
|
-
create_index_if_not_exists(:entity_aliases, :entity_id, :idx_entity_aliases_entity)
|
|
188
|
-
create_index_if_not_exists(:content_items, :session_id, :idx_content_items_session)
|
|
189
|
-
create_index_if_not_exists(:content_items, :project_path, :idx_content_items_project)
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def create_index_if_not_exists(table, column, name)
|
|
193
|
-
@db.run("CREATE INDEX IF NOT EXISTS #{name} ON #{table}(#{column})")
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def set_meta(key, value)
|
|
197
|
-
@db[:meta].insert_conflict(target: :key, update: {value: value}).insert(key: key, value: value)
|
|
198
|
-
end
|
|
199
|
-
|
|
200
|
-
def get_meta(key)
|
|
201
|
-
@db[:meta].where(key: key).get(:value)
|
|
109
|
+
def ingestion_metrics
|
|
110
|
+
@db[:ingestion_metrics]
|
|
202
111
|
end
|
|
203
112
|
|
|
204
|
-
public
|
|
205
|
-
|
|
206
113
|
def upsert_content_item(source:, text_hash:, byte_len:, session_id: nil, transcript_path: nil,
|
|
207
|
-
project_path: nil, occurred_at: nil, raw_text: nil, metadata: nil
|
|
114
|
+
project_path: nil, occurred_at: nil, raw_text: nil, metadata: nil,
|
|
115
|
+
git_branch: nil, cwd: nil, claude_version: nil, thinking_level: nil, source_mtime: nil)
|
|
208
116
|
existing = content_items.where(text_hash: text_hash, session_id: session_id).get(:id)
|
|
209
117
|
return existing if existing
|
|
210
118
|
|
|
@@ -219,10 +127,41 @@ module ClaudeMemory
|
|
|
219
127
|
text_hash: text_hash,
|
|
220
128
|
byte_len: byte_len,
|
|
221
129
|
raw_text: raw_text,
|
|
222
|
-
metadata_json: metadata&.to_json
|
|
130
|
+
metadata_json: metadata&.to_json,
|
|
131
|
+
git_branch: git_branch,
|
|
132
|
+
cwd: cwd,
|
|
133
|
+
claude_version: claude_version,
|
|
134
|
+
thinking_level: thinking_level,
|
|
135
|
+
source_mtime: source_mtime
|
|
223
136
|
)
|
|
224
137
|
end
|
|
225
138
|
|
|
139
|
+
def content_item_by_transcript_and_mtime(transcript_path, mtime_iso8601)
|
|
140
|
+
content_items
|
|
141
|
+
.where(transcript_path: transcript_path, source_mtime: mtime_iso8601)
|
|
142
|
+
.first
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def insert_tool_calls(content_item_id, tool_calls_data)
|
|
146
|
+
tool_calls_data.each do |tc|
|
|
147
|
+
tool_calls.insert(
|
|
148
|
+
content_item_id: content_item_id,
|
|
149
|
+
tool_name: tc[:tool_name],
|
|
150
|
+
tool_input: tc[:tool_input],
|
|
151
|
+
tool_result: tc[:tool_result],
|
|
152
|
+
is_error: tc[:is_error] || false,
|
|
153
|
+
timestamp: tc[:timestamp]
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def tool_calls_for_content_item(content_item_id)
|
|
159
|
+
tool_calls
|
|
160
|
+
.where(content_item_id: content_item_id)
|
|
161
|
+
.order(:timestamp)
|
|
162
|
+
.all
|
|
163
|
+
end
|
|
164
|
+
|
|
226
165
|
def get_delta_cursor(session_id, transcript_path)
|
|
227
166
|
delta_cursors.where(session_id: session_id, transcript_path: transcript_path).get(:last_byte_offset)
|
|
228
167
|
end
|
|
@@ -272,7 +211,7 @@ module ClaudeMemory
|
|
|
272
211
|
)
|
|
273
212
|
end
|
|
274
213
|
|
|
275
|
-
def update_fact(fact_id, status: nil, valid_to: nil, scope: nil, project_path: nil)
|
|
214
|
+
def update_fact(fact_id, status: nil, valid_to: nil, scope: nil, project_path: nil, embedding: nil)
|
|
276
215
|
updates = {}
|
|
277
216
|
updates[:status] = status if status
|
|
278
217
|
updates[:valid_to] = valid_to if valid_to
|
|
@@ -282,12 +221,29 @@ module ClaudeMemory
|
|
|
282
221
|
updates[:project_path] = (scope == "global") ? nil : project_path
|
|
283
222
|
end
|
|
284
223
|
|
|
224
|
+
if embedding
|
|
225
|
+
updates[:embedding_json] = embedding.to_json
|
|
226
|
+
end
|
|
227
|
+
|
|
285
228
|
return false if updates.empty?
|
|
286
229
|
|
|
287
230
|
facts.where(id: fact_id).update(updates)
|
|
288
231
|
true
|
|
289
232
|
end
|
|
290
233
|
|
|
234
|
+
def update_fact_embedding(fact_id, embedding_vector)
|
|
235
|
+
facts.where(id: fact_id).update(embedding_json: embedding_vector.to_json)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def facts_with_embeddings(limit: 1000)
|
|
239
|
+
facts
|
|
240
|
+
.where(Sequel.~(embedding_json: nil))
|
|
241
|
+
.where(status: "active")
|
|
242
|
+
.select(:id, :subject_entity_id, :predicate, :object_literal, :embedding_json, :scope)
|
|
243
|
+
.limit(limit)
|
|
244
|
+
.all
|
|
245
|
+
end
|
|
246
|
+
|
|
291
247
|
def facts_for_slot(subject_entity_id, predicate, status: "active")
|
|
292
248
|
facts
|
|
293
249
|
.where(subject_entity_id: subject_entity_id, predicate: predicate, status: status)
|
|
@@ -330,8 +286,118 @@ module ClaudeMemory
|
|
|
330
286
|
fact_links.insert(from_fact_id: from_fact_id, to_fact_id: to_fact_id, link_type: link_type)
|
|
331
287
|
end
|
|
332
288
|
|
|
289
|
+
# Record token usage metrics for a distillation operation
|
|
290
|
+
#
|
|
291
|
+
# @param content_item_id [Integer] The content item that was distilled
|
|
292
|
+
# @param input_tokens [Integer] Tokens sent to the API
|
|
293
|
+
# @param output_tokens [Integer] Tokens returned from the API
|
|
294
|
+
# @param facts_extracted [Integer] Number of facts extracted
|
|
295
|
+
# @return [Integer] The created metric record ID
|
|
296
|
+
def record_ingestion_metrics(content_item_id:, input_tokens:, output_tokens:, facts_extracted:)
|
|
297
|
+
ingestion_metrics.insert(
|
|
298
|
+
content_item_id: content_item_id,
|
|
299
|
+
input_tokens: input_tokens,
|
|
300
|
+
output_tokens: output_tokens,
|
|
301
|
+
facts_extracted: facts_extracted,
|
|
302
|
+
created_at: Time.now.utc.iso8601
|
|
303
|
+
)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Get aggregate metrics across all distillation operations
|
|
307
|
+
#
|
|
308
|
+
# @return [Hash] Aggregated metrics with keys:
|
|
309
|
+
# - total_input_tokens: Total tokens sent to API
|
|
310
|
+
# - total_output_tokens: Total tokens returned from API
|
|
311
|
+
# - total_facts_extracted: Total facts extracted
|
|
312
|
+
# - total_operations: Number of distillation operations
|
|
313
|
+
# - avg_facts_per_1k_input_tokens: Average efficiency metric
|
|
314
|
+
def aggregate_ingestion_metrics
|
|
315
|
+
# standard:disable Performance/Detect (Sequel DSL requires .select{}.first)
|
|
316
|
+
result = ingestion_metrics
|
|
317
|
+
.select {
|
|
318
|
+
[
|
|
319
|
+
sum(:input_tokens).as(:total_input),
|
|
320
|
+
sum(:output_tokens).as(:total_output),
|
|
321
|
+
sum(:facts_extracted).as(:total_facts),
|
|
322
|
+
count(:id).as(:total_ops)
|
|
323
|
+
]
|
|
324
|
+
}
|
|
325
|
+
.first
|
|
326
|
+
# standard:enable Performance/Detect
|
|
327
|
+
|
|
328
|
+
return nil if result.nil? || result[:total_ops].to_i.zero?
|
|
329
|
+
|
|
330
|
+
total_input = result[:total_input].to_i
|
|
331
|
+
total_output = result[:total_output].to_i
|
|
332
|
+
total_facts = result[:total_facts].to_i
|
|
333
|
+
total_ops = result[:total_ops].to_i
|
|
334
|
+
|
|
335
|
+
efficiency = total_input.zero? ? 0.0 : (total_facts.to_f / total_input * 1000).round(2)
|
|
336
|
+
|
|
337
|
+
{
|
|
338
|
+
total_input_tokens: total_input,
|
|
339
|
+
total_output_tokens: total_output,
|
|
340
|
+
total_facts_extracted: total_facts,
|
|
341
|
+
total_operations: total_ops,
|
|
342
|
+
avg_facts_per_1k_input_tokens: efficiency
|
|
343
|
+
}
|
|
344
|
+
end
|
|
345
|
+
|
|
333
346
|
private
|
|
334
347
|
|
|
348
|
+
def ensure_schema!
|
|
349
|
+
migrations_path = File.expand_path("../../../db/migrations", __dir__)
|
|
350
|
+
|
|
351
|
+
# Handle backward compatibility: databases created with old migration system
|
|
352
|
+
sync_legacy_schema_version!
|
|
353
|
+
|
|
354
|
+
# Run Sequel migrations to bring database to target version
|
|
355
|
+
Sequel::Migrator.run(@db, migrations_path, target: SCHEMA_VERSION)
|
|
356
|
+
|
|
357
|
+
# Set created_at timestamp on first initialization
|
|
358
|
+
set_meta("created_at", Time.now.utc.iso8601) unless get_meta("created_at")
|
|
359
|
+
|
|
360
|
+
# Sync legacy schema_version meta key with Sequel's schema_info
|
|
361
|
+
# This maintains backwards compatibility with code that reads schema_version
|
|
362
|
+
sequel_version = @db[:schema_info].get(:version) if @db.table_exists?(:schema_info)
|
|
363
|
+
set_meta("schema_version", sequel_version.to_s) if sequel_version
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Sync legacy schema_version from meta table to Sequel's schema_info
|
|
367
|
+
# Handles two cases:
|
|
368
|
+
# 1. No schema_info table exists (old system, pre-Sequel migrations)
|
|
369
|
+
# 2. schema_info exists but is out of sync with meta.schema_version
|
|
370
|
+
def sync_legacy_schema_version!
|
|
371
|
+
return unless @db.table_exists?(:meta)
|
|
372
|
+
|
|
373
|
+
meta_version = get_meta("schema_version")&.to_i
|
|
374
|
+
return unless meta_version && meta_version >= 2
|
|
375
|
+
|
|
376
|
+
# Verify database actually has v2+ schema (defensive check)
|
|
377
|
+
columns = @db.schema(:content_items).map(&:first) if @db.table_exists?(:content_items)
|
|
378
|
+
return unless columns&.include?(:project_path)
|
|
379
|
+
|
|
380
|
+
# Create or update schema_info to match meta.schema_version
|
|
381
|
+
@db.create_table?(:schema_info) do
|
|
382
|
+
Integer :version, null: false, default: 0
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
sequel_version = @db[:schema_info].get(:version)
|
|
386
|
+
if sequel_version.nil? || sequel_version < meta_version
|
|
387
|
+
# Update schema_info to match meta (old system's version)
|
|
388
|
+
@db[:schema_info].delete
|
|
389
|
+
@db[:schema_info].insert(version: meta_version)
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
def set_meta(key, value)
|
|
394
|
+
@db[:meta].insert_conflict(target: :key, update: {value: value}).insert(key: key, value: value)
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def get_meta(key)
|
|
398
|
+
@db[:meta].where(key: key).get(:value)
|
|
399
|
+
end
|
|
400
|
+
|
|
335
401
|
def slugify(type, name)
|
|
336
402
|
"#{type}:#{name.downcase.gsub(/[^a-z0-9]+/, "_").gsub(/^_|_$/, "")}"
|
|
337
403
|
end
|
|
@@ -8,21 +8,21 @@ module ClaudeMemory
|
|
|
8
8
|
attr_reader :global_store, :project_store, :project_path
|
|
9
9
|
|
|
10
10
|
def initialize(global_db_path: nil, project_db_path: nil, project_path: nil, env: ENV)
|
|
11
|
-
|
|
12
|
-
@
|
|
13
|
-
@
|
|
11
|
+
config = Configuration.new(env)
|
|
12
|
+
@project_path = project_path || config.project_dir
|
|
13
|
+
@global_db_path = global_db_path || config.global_db_path
|
|
14
|
+
@project_db_path = project_db_path || config.project_db_path(@project_path)
|
|
14
15
|
|
|
15
16
|
@global_store = nil
|
|
16
17
|
@project_store = nil
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def self.default_global_db_path(env = ENV)
|
|
20
|
-
|
|
21
|
-
File.join(home, ".claude", "memory.sqlite3")
|
|
21
|
+
Configuration.new(env).global_db_path
|
|
22
22
|
end
|
|
23
23
|
|
|
24
24
|
def self.default_project_db_path(project_path = Dir.pwd)
|
|
25
|
-
|
|
25
|
+
Configuration.new.project_db_path(project_path)
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def ensure_global!
|
|
@@ -31,6 +31,7 @@ module ClaudeMemory
|
|
|
31
31
|
expire_disputed_facts if within_budget?
|
|
32
32
|
prune_orphaned_provenance if within_budget?
|
|
33
33
|
prune_old_content if within_budget?
|
|
34
|
+
checkpoint_wal if within_budget?
|
|
34
35
|
|
|
35
36
|
@stats[:elapsed_seconds] = Time.now - @start_time
|
|
36
37
|
@stats[:budget_honored] = @stats[:elapsed_seconds] <= budget
|
|
@@ -75,6 +76,11 @@ module ClaudeMemory
|
|
|
75
76
|
.exclude(id: referenced_ids)
|
|
76
77
|
.delete
|
|
77
78
|
end
|
|
79
|
+
|
|
80
|
+
def checkpoint_wal
|
|
81
|
+
@store.checkpoint_wal
|
|
82
|
+
@stats[:wal_checkpointed] = true
|
|
83
|
+
end
|
|
78
84
|
end
|
|
79
85
|
end
|
|
80
86
|
end
|