claude_memory 0.7.1 → 0.8.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/memory.sqlite3-shm +0 -0
- data/.claude/memory.sqlite3-wal +0 -0
- data/.claude/settings.json +78 -6
- data/.claude/settings.local.json +2 -1
- data/.claude/skills/improve/SKILL.md +113 -25
- data/.claude-plugin/commands/distill-transcripts.md +98 -0
- data/.claude-plugin/commands/memory-recall.md +67 -0
- data/.claude-plugin/marketplace.json +1 -1
- data/.claude-plugin/plugin.json +1 -1
- data/CHANGELOG.md +49 -1
- data/CLAUDE.md +29 -5
- data/docs/improvements.md +18 -56
- data/docs/quality_review.md +119 -224
- data/hooks/hooks.json +39 -7
- data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
- data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
- data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
- data/lib/claude_memory/commands/completion_command.rb +179 -0
- data/lib/claude_memory/commands/doctor_command.rb +2 -0
- data/lib/claude_memory/commands/help_command.rb +4 -0
- data/lib/claude_memory/commands/hook_command.rb +2 -1
- data/lib/claude_memory/commands/index_command.rb +85 -78
- data/lib/claude_memory/commands/initializers/database_ensurer.rb +16 -0
- data/lib/claude_memory/commands/initializers/global_initializer.rb +2 -1
- data/lib/claude_memory/commands/initializers/hooks_configurator.rb +55 -11
- data/lib/claude_memory/commands/initializers/project_initializer.rb +2 -1
- data/lib/claude_memory/commands/install_skill_command.rb +78 -0
- data/lib/claude_memory/commands/registry.rb +3 -1
- data/lib/claude_memory/commands/skills/distill-transcripts.md +98 -0
- data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
- data/lib/claude_memory/core/fact_ranker.rb +2 -2
- data/lib/claude_memory/core/rr_fusion.rb +23 -6
- data/lib/claude_memory/core/snippet_extractor.rb +7 -3
- data/lib/claude_memory/core/text_builder.rb +11 -0
- data/lib/claude_memory/domain/provenance.rb +0 -1
- data/lib/claude_memory/embeddings/api_adapter.rb +96 -0
- data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
- data/lib/claude_memory/embeddings/fastembed_adapter.rb +4 -0
- data/lib/claude_memory/embeddings/generator.rb +4 -0
- data/lib/claude_memory/embeddings/resolver.rb +18 -0
- data/lib/claude_memory/hook/context_injector.rb +58 -2
- data/lib/claude_memory/hook/distillation_runner.rb +46 -0
- data/lib/claude_memory/hook/handler.rb +11 -2
- data/lib/claude_memory/index/vector_index.rb +15 -2
- data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
- data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
- data/lib/claude_memory/mcp/handlers/management_handlers.rb +145 -0
- data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
- data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
- data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
- data/lib/claude_memory/mcp/handlers/stats_handlers.rb +202 -0
- data/lib/claude_memory/mcp/instructions_builder.rb +2 -1
- data/lib/claude_memory/mcp/query_guide.rb +10 -0
- data/lib/claude_memory/mcp/response_formatter.rb +1 -0
- data/lib/claude_memory/mcp/text_summary.rb +26 -0
- data/lib/claude_memory/mcp/tool_definitions.rb +30 -1
- data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
- data/lib/claude_memory/mcp/tools.rb +39 -678
- data/lib/claude_memory/recall/dual_engine.rb +105 -0
- data/lib/claude_memory/recall/legacy_engine.rb +138 -0
- data/lib/claude_memory/recall/query_core.rb +371 -0
- data/lib/claude_memory/recall.rb +29 -662
- data/lib/claude_memory/shortcuts.rb +4 -4
- data/lib/claude_memory/store/retry_handler.rb +61 -0
- data/lib/claude_memory/store/schema_manager.rb +68 -0
- data/lib/claude_memory/store/sqlite_store.rb +85 -201
- data/lib/claude_memory/templates/hooks.example.json +26 -7
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +11 -0
- metadata +23 -1
|
@@ -5,22 +5,22 @@ module ClaudeMemory
|
|
|
5
5
|
QUERIES = {
|
|
6
6
|
decisions: {
|
|
7
7
|
query: "decision constraint rule requirement",
|
|
8
|
-
scope:
|
|
8
|
+
scope: "all",
|
|
9
9
|
limit: 10
|
|
10
10
|
},
|
|
11
11
|
architecture: {
|
|
12
12
|
query: "uses framework implements architecture pattern",
|
|
13
|
-
scope:
|
|
13
|
+
scope: "all",
|
|
14
14
|
limit: 10
|
|
15
15
|
},
|
|
16
16
|
conventions: {
|
|
17
17
|
query: "convention style format pattern prefer",
|
|
18
|
-
scope:
|
|
18
|
+
scope: "global",
|
|
19
19
|
limit: 20
|
|
20
20
|
},
|
|
21
21
|
project_config: {
|
|
22
22
|
query: "uses requires depends_on configuration",
|
|
23
|
-
scope:
|
|
23
|
+
scope: "project",
|
|
24
24
|
limit: 10
|
|
25
25
|
}
|
|
26
26
|
}.freeze
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module Store
|
|
5
|
+
# Retry logic for SQLite database operations.
|
|
6
|
+
# Handles busy/locked errors from concurrent access by multiple hook processes.
|
|
7
|
+
module RetryHandler
|
|
8
|
+
MAX_RETRIES = 5
|
|
9
|
+
RETRY_BASE_DELAY = 0.1 # seconds, with exponential backoff
|
|
10
|
+
|
|
11
|
+
def with_retry(operation_name = "database operation")
|
|
12
|
+
retries = 0
|
|
13
|
+
begin
|
|
14
|
+
yield
|
|
15
|
+
rescue Sequel::DatabaseError, Extralite::Error, Extralite::BusyError => e
|
|
16
|
+
if retryable_error?(e) && retries < MAX_RETRIES
|
|
17
|
+
retries += 1
|
|
18
|
+
delay = RETRY_BASE_DELAY * (2**retries)
|
|
19
|
+
sleep(delay)
|
|
20
|
+
retry
|
|
21
|
+
end
|
|
22
|
+
raise
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def transaction_with_retry(&block)
|
|
27
|
+
with_retry("transaction") do
|
|
28
|
+
@db.transaction(&block)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def retryable_error?(error)
|
|
35
|
+
message = error.message.downcase
|
|
36
|
+
message.include?("busy") || message.include?("locked")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def connect_database(db_path)
|
|
40
|
+
retries = 0
|
|
41
|
+
begin
|
|
42
|
+
Sequel.connect(
|
|
43
|
+
"extralite:#{db_path}",
|
|
44
|
+
connect_sqls: [
|
|
45
|
+
"PRAGMA busy_timeout = 1000",
|
|
46
|
+
"PRAGMA journal_mode = WAL",
|
|
47
|
+
"PRAGMA synchronous = NORMAL"
|
|
48
|
+
]
|
|
49
|
+
)
|
|
50
|
+
rescue Sequel::DatabaseConnectionError, Extralite::Error => e
|
|
51
|
+
retries += 1
|
|
52
|
+
if retries <= MAX_RETRIES && retryable_error?(e)
|
|
53
|
+
sleep(RETRY_BASE_DELAY * (2**retries))
|
|
54
|
+
retry
|
|
55
|
+
end
|
|
56
|
+
raise
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClaudeMemory
|
|
4
|
+
module Store
|
|
5
|
+
# Schema migration and version management for SQLiteStore.
|
|
6
|
+
# Handles Sequel migrations, legacy version syncing, and initial setup.
|
|
7
|
+
module SchemaManager
|
|
8
|
+
SCHEMA_VERSION = 12
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
def ensure_schema!
|
|
13
|
+
migrations_path = File.expand_path("../../../db/migrations", __dir__)
|
|
14
|
+
|
|
15
|
+
# Handle backward compatibility: databases created with old migration system
|
|
16
|
+
sync_legacy_schema_version!
|
|
17
|
+
|
|
18
|
+
# Skip migration if the database is already ahead of this gem's version.
|
|
19
|
+
# This happens when a newer gem version migrated the DB and an older
|
|
20
|
+
# installed gem (e.g. via hooks) tries to open it.
|
|
21
|
+
current = current_schema_version
|
|
22
|
+
return if current && current > SCHEMA_VERSION
|
|
23
|
+
|
|
24
|
+
# Run Sequel migrations to bring database to target version
|
|
25
|
+
Sequel::Migrator.run(@db, migrations_path, target: SCHEMA_VERSION)
|
|
26
|
+
|
|
27
|
+
# Set created_at timestamp on first initialization
|
|
28
|
+
set_meta("created_at", Time.now.utc.iso8601) unless get_meta("created_at")
|
|
29
|
+
|
|
30
|
+
# Sync legacy schema_version meta key with Sequel's schema_info
|
|
31
|
+
# This maintains backwards compatibility with code that reads schema_version
|
|
32
|
+
sequel_version = @db[:schema_info].get(:version) if @db.table_exists?(:schema_info)
|
|
33
|
+
set_meta("schema_version", sequel_version.to_s) if sequel_version
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Sync legacy schema_version from meta table to Sequel's schema_info
|
|
37
|
+
# Handles two cases:
|
|
38
|
+
# 1. No schema_info table exists (old system, pre-Sequel migrations)
|
|
39
|
+
# 2. schema_info exists but is out of sync with meta.schema_version
|
|
40
|
+
def sync_legacy_schema_version!
|
|
41
|
+
return unless @db.table_exists?(:meta)
|
|
42
|
+
|
|
43
|
+
meta_version = get_meta("schema_version")&.to_i
|
|
44
|
+
return unless meta_version && meta_version >= 2
|
|
45
|
+
|
|
46
|
+
# Verify database actually has v2+ schema (defensive check)
|
|
47
|
+
columns = @db.schema(:content_items).map(&:first) if @db.table_exists?(:content_items)
|
|
48
|
+
return unless columns&.include?(:project_path)
|
|
49
|
+
|
|
50
|
+
# Create or update schema_info to match meta.schema_version
|
|
51
|
+
@db.create_table?(:schema_info) do
|
|
52
|
+
Integer :version, null: false, default: 0
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
sequel_version = @db[:schema_info].get(:version)
|
|
56
|
+
if sequel_version.nil? || sequel_version < meta_version
|
|
57
|
+
@db[:schema_info].delete
|
|
58
|
+
@db[:schema_info].insert(version: meta_version)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def current_schema_version
|
|
63
|
+
return nil unless @db.table_exists?(:schema_info)
|
|
64
|
+
@db[:schema_info].get(:version)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -6,11 +6,14 @@ require "digest"
|
|
|
6
6
|
require "json"
|
|
7
7
|
require "extralite"
|
|
8
8
|
require "sequel/adapters/extralite"
|
|
9
|
+
require_relative "retry_handler"
|
|
10
|
+
require_relative "schema_manager"
|
|
9
11
|
|
|
10
12
|
module ClaudeMemory
|
|
11
13
|
module Store
|
|
12
14
|
class SQLiteStore
|
|
13
|
-
|
|
15
|
+
include RetryHandler
|
|
16
|
+
include SchemaManager
|
|
14
17
|
|
|
15
18
|
attr_reader :db
|
|
16
19
|
|
|
@@ -21,69 +24,6 @@ module ClaudeMemory
|
|
|
21
24
|
ensure_schema!
|
|
22
25
|
end
|
|
23
26
|
|
|
24
|
-
# Retry configuration for database operations
|
|
25
|
-
# SQLite's busy_timeout doesn't reliably detect lock release, so we use
|
|
26
|
-
# shorter timeouts with application-level retry for better responsiveness
|
|
27
|
-
MAX_RETRIES = 5
|
|
28
|
-
RETRY_BASE_DELAY = 0.1 # seconds, with exponential backoff
|
|
29
|
-
|
|
30
|
-
# Execute a block with retry logic for busy/locked errors
|
|
31
|
-
# This handles concurrent access from multiple hook processes
|
|
32
|
-
def with_retry(operation_name = "database operation")
|
|
33
|
-
retries = 0
|
|
34
|
-
begin
|
|
35
|
-
yield
|
|
36
|
-
rescue Sequel::DatabaseError, Extralite::Error, Extralite::BusyError => e
|
|
37
|
-
if retryable_error?(e) && retries < MAX_RETRIES
|
|
38
|
-
retries += 1
|
|
39
|
-
delay = RETRY_BASE_DELAY * (2**retries) # Exponential backoff
|
|
40
|
-
sleep(delay)
|
|
41
|
-
retry
|
|
42
|
-
end
|
|
43
|
-
raise
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Execute a transaction with retry logic for concurrent access
|
|
48
|
-
# Use this instead of @db.transaction when concurrent writes are expected
|
|
49
|
-
def transaction_with_retry(&block)
|
|
50
|
-
with_retry("transaction") do
|
|
51
|
-
@db.transaction(&block)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
def retryable_error?(error)
|
|
58
|
-
message = error.message.downcase
|
|
59
|
-
message.include?("busy") || message.include?("locked")
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def connect_database(db_path)
|
|
63
|
-
retries = 0
|
|
64
|
-
begin
|
|
65
|
-
Sequel.connect(
|
|
66
|
-
"extralite:#{db_path}",
|
|
67
|
-
# Use shorter busy_timeout since we handle retry at app level
|
|
68
|
-
# This allows faster detection of lock release between retries
|
|
69
|
-
connect_sqls: [
|
|
70
|
-
"PRAGMA busy_timeout = 1000",
|
|
71
|
-
"PRAGMA journal_mode = WAL",
|
|
72
|
-
"PRAGMA synchronous = NORMAL"
|
|
73
|
-
]
|
|
74
|
-
)
|
|
75
|
-
rescue Sequel::DatabaseConnectionError, Extralite::Error => e
|
|
76
|
-
retries += 1
|
|
77
|
-
if retries <= MAX_RETRIES && retryable_error?(e)
|
|
78
|
-
sleep(RETRY_BASE_DELAY * (2**retries))
|
|
79
|
-
retry
|
|
80
|
-
end
|
|
81
|
-
raise
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
public
|
|
86
|
-
|
|
87
27
|
def close
|
|
88
28
|
@db.disconnect
|
|
89
29
|
end
|
|
@@ -93,8 +33,6 @@ module ClaudeMemory
|
|
|
93
33
|
end
|
|
94
34
|
|
|
95
35
|
# Checkpoint the WAL file to prevent unlimited growth
|
|
96
|
-
# This truncates the WAL after checkpointing
|
|
97
|
-
# Should be called periodically during maintenance/sweep operations
|
|
98
36
|
def checkpoint_wal
|
|
99
37
|
@db.run("PRAGMA wal_checkpoint(TRUNCATE)")
|
|
100
38
|
end
|
|
@@ -103,57 +41,35 @@ module ClaudeMemory
|
|
|
103
41
|
@db[:meta].where(key: "schema_version").get(:value)&.to_i
|
|
104
42
|
end
|
|
105
43
|
|
|
106
|
-
|
|
107
|
-
@db[:content_items]
|
|
108
|
-
end
|
|
44
|
+
# --- Table accessors ---
|
|
109
45
|
|
|
110
|
-
def
|
|
111
|
-
@db[:delta_cursors]
|
|
112
|
-
end
|
|
46
|
+
def content_items = @db[:content_items]
|
|
113
47
|
|
|
114
|
-
def
|
|
115
|
-
@db[:entities]
|
|
116
|
-
end
|
|
48
|
+
def delta_cursors = @db[:delta_cursors]
|
|
117
49
|
|
|
118
|
-
def
|
|
119
|
-
@db[:entity_aliases]
|
|
120
|
-
end
|
|
50
|
+
def entities = @db[:entities]
|
|
121
51
|
|
|
122
|
-
def
|
|
123
|
-
@db[:facts]
|
|
124
|
-
end
|
|
52
|
+
def entity_aliases = @db[:entity_aliases]
|
|
125
53
|
|
|
126
|
-
def
|
|
127
|
-
@db[:provenance]
|
|
128
|
-
end
|
|
54
|
+
def facts = @db[:facts]
|
|
129
55
|
|
|
130
|
-
def
|
|
131
|
-
@db[:fact_links]
|
|
132
|
-
end
|
|
56
|
+
def provenance = @db[:provenance]
|
|
133
57
|
|
|
134
|
-
def
|
|
135
|
-
@db[:conflicts]
|
|
136
|
-
end
|
|
58
|
+
def fact_links = @db[:fact_links]
|
|
137
59
|
|
|
138
|
-
def
|
|
139
|
-
@db[:tool_calls]
|
|
140
|
-
end
|
|
60
|
+
def conflicts = @db[:conflicts]
|
|
141
61
|
|
|
142
|
-
def
|
|
143
|
-
@db[:operation_progress]
|
|
144
|
-
end
|
|
62
|
+
def tool_calls = @db[:tool_calls]
|
|
145
63
|
|
|
146
|
-
def
|
|
147
|
-
@db[:schema_health]
|
|
148
|
-
end
|
|
64
|
+
def operation_progress = @db[:operation_progress]
|
|
149
65
|
|
|
150
|
-
def
|
|
151
|
-
@db[:ingestion_metrics]
|
|
152
|
-
end
|
|
66
|
+
def schema_health = @db[:schema_health]
|
|
153
67
|
|
|
154
|
-
def
|
|
155
|
-
|
|
156
|
-
|
|
68
|
+
def ingestion_metrics = @db[:ingestion_metrics]
|
|
69
|
+
|
|
70
|
+
def llm_cache = @db[:llm_cache]
|
|
71
|
+
|
|
72
|
+
# --- Content items ---
|
|
157
73
|
|
|
158
74
|
def upsert_content_item(source:, text_hash:, byte_len:, session_id: nil, transcript_path: nil,
|
|
159
75
|
project_path: nil, occurred_at: nil, raw_text: nil, metadata: nil,
|
|
@@ -183,12 +99,18 @@ module ClaudeMemory
|
|
|
183
99
|
end
|
|
184
100
|
end
|
|
185
101
|
|
|
102
|
+
def get_content_item(id)
|
|
103
|
+
content_items.where(id: id).first
|
|
104
|
+
end
|
|
105
|
+
|
|
186
106
|
def content_item_by_transcript_and_mtime(transcript_path, mtime_iso8601)
|
|
187
107
|
content_items
|
|
188
108
|
.where(transcript_path: transcript_path, source_mtime: mtime_iso8601)
|
|
189
109
|
.first
|
|
190
110
|
end
|
|
191
111
|
|
|
112
|
+
# --- Tool calls ---
|
|
113
|
+
|
|
192
114
|
def insert_tool_calls(content_item_id, tool_calls_data)
|
|
193
115
|
tool_calls_data.each do |tc|
|
|
194
116
|
tool_calls.insert(
|
|
@@ -210,6 +132,8 @@ module ClaudeMemory
|
|
|
210
132
|
.all
|
|
211
133
|
end
|
|
212
134
|
|
|
135
|
+
# --- Delta cursors ---
|
|
136
|
+
|
|
213
137
|
def get_delta_cursor(session_id, transcript_path)
|
|
214
138
|
delta_cursors.where(session_id: session_id, transcript_path: transcript_path).get(:last_byte_offset)
|
|
215
139
|
end
|
|
@@ -229,6 +153,8 @@ module ClaudeMemory
|
|
|
229
153
|
)
|
|
230
154
|
end
|
|
231
155
|
|
|
156
|
+
# --- Entities ---
|
|
157
|
+
|
|
232
158
|
def find_or_create_entity(type:, name:)
|
|
233
159
|
slug = slugify(type, name)
|
|
234
160
|
existing = entities.where(slug: slug).get(:id)
|
|
@@ -238,6 +164,8 @@ module ClaudeMemory
|
|
|
238
164
|
entities.insert(type: type, canonical_name: name, slug: slug, created_at: now)
|
|
239
165
|
end
|
|
240
166
|
|
|
167
|
+
# --- Facts ---
|
|
168
|
+
|
|
241
169
|
def insert_fact(subject_entity_id:, predicate:, object_entity_id: nil, object_literal: nil,
|
|
242
170
|
datatype: nil, polarity: "positive", valid_from: nil, status: "active",
|
|
243
171
|
confidence: 1.0, created_from: nil, scope: "project", project_path: nil)
|
|
@@ -307,6 +235,8 @@ module ClaudeMemory
|
|
|
307
235
|
.all
|
|
308
236
|
end
|
|
309
237
|
|
|
238
|
+
# --- Provenance ---
|
|
239
|
+
|
|
310
240
|
def insert_provenance(fact_id:, content_item_id: nil, quote: nil, attribution_entity_id: nil, strength: "stated",
|
|
311
241
|
line_start: nil, line_end: nil)
|
|
312
242
|
provenance.insert(
|
|
@@ -324,6 +254,8 @@ module ClaudeMemory
|
|
|
324
254
|
provenance.where(fact_id: fact_id).all
|
|
325
255
|
end
|
|
326
256
|
|
|
257
|
+
# --- Conflicts & fact links ---
|
|
258
|
+
|
|
327
259
|
def insert_conflict(fact_a_id:, fact_b_id:, status: "open", notes: nil)
|
|
328
260
|
now = Time.now.utc.iso8601
|
|
329
261
|
conflicts.insert(
|
|
@@ -343,13 +275,27 @@ module ClaudeMemory
|
|
|
343
275
|
fact_links.insert(from_fact_id: from_fact_id, to_fact_id: to_fact_id, link_type: link_type)
|
|
344
276
|
end
|
|
345
277
|
|
|
346
|
-
#
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
278
|
+
# --- Ingestion metrics ---
|
|
279
|
+
|
|
280
|
+
def undistilled_content_items(limit: 3, min_length: 200)
|
|
281
|
+
content_items
|
|
282
|
+
.left_join(:ingestion_metrics, content_item_id: :id)
|
|
283
|
+
.where(Sequel[:ingestion_metrics][:id] => nil)
|
|
284
|
+
.where { byte_len >= min_length }
|
|
285
|
+
.order(Sequel.desc(:occurred_at))
|
|
286
|
+
.limit(limit)
|
|
287
|
+
.select_all(:content_items)
|
|
288
|
+
.all
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def count_undistilled(min_length: 200)
|
|
292
|
+
content_items
|
|
293
|
+
.left_join(:ingestion_metrics, content_item_id: :id)
|
|
294
|
+
.where(Sequel[:ingestion_metrics][:id] => nil)
|
|
295
|
+
.where { byte_len >= min_length }
|
|
296
|
+
.count
|
|
297
|
+
end
|
|
298
|
+
|
|
353
299
|
def record_ingestion_metrics(content_item_id:, input_tokens:, output_tokens:, facts_extracted:)
|
|
354
300
|
ingestion_metrics.insert(
|
|
355
301
|
content_item_id: content_item_id,
|
|
@@ -360,14 +306,6 @@ module ClaudeMemory
|
|
|
360
306
|
)
|
|
361
307
|
end
|
|
362
308
|
|
|
363
|
-
# Get aggregate metrics across all distillation operations
|
|
364
|
-
#
|
|
365
|
-
# @return [Hash] Aggregated metrics with keys:
|
|
366
|
-
# - total_input_tokens: Total tokens sent to API
|
|
367
|
-
# - total_output_tokens: Total tokens returned from API
|
|
368
|
-
# - total_facts_extracted: Total facts extracted
|
|
369
|
-
# - total_operations: Number of distillation operations
|
|
370
|
-
# - avg_facts_per_1k_input_tokens: Average efficiency metric
|
|
371
309
|
def aggregate_ingestion_metrics
|
|
372
310
|
# standard:disable Performance/Detect (Sequel DSL requires .select{}.first)
|
|
373
311
|
result = ingestion_metrics
|
|
@@ -400,23 +338,34 @@ module ClaudeMemory
|
|
|
400
338
|
}
|
|
401
339
|
end
|
|
402
340
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
341
|
+
def backfill_distillation_metrics!
|
|
342
|
+
undistilled_ids = content_items
|
|
343
|
+
.left_join(:ingestion_metrics, content_item_id: :id)
|
|
344
|
+
.where(Sequel[:ingestion_metrics][:id] => nil)
|
|
345
|
+
.select_map(Sequel[:content_items][:id])
|
|
346
|
+
|
|
347
|
+
return 0 if undistilled_ids.empty?
|
|
348
|
+
|
|
349
|
+
now = Time.now.utc.iso8601
|
|
350
|
+
undistilled_ids.each do |cid|
|
|
351
|
+
ingestion_metrics.insert(
|
|
352
|
+
content_item_id: cid,
|
|
353
|
+
input_tokens: 0,
|
|
354
|
+
output_tokens: 0,
|
|
355
|
+
facts_extracted: 0,
|
|
356
|
+
created_at: now
|
|
357
|
+
)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
undistilled_ids.size
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# --- LLM cache ---
|
|
364
|
+
|
|
407
365
|
def llm_cache_lookup(cache_key)
|
|
408
366
|
llm_cache.where(cache_key: cache_key).first
|
|
409
367
|
end
|
|
410
368
|
|
|
411
|
-
# Store an LLM response in the cache
|
|
412
|
-
#
|
|
413
|
-
# @param operation [String] Operation type (e.g., "distill", "extract")
|
|
414
|
-
# @param model [String] Model identifier
|
|
415
|
-
# @param input_hash [String] SHA256 of input content
|
|
416
|
-
# @param result_json [String] JSON response to cache
|
|
417
|
-
# @param input_tokens [Integer, nil] Tokens in request
|
|
418
|
-
# @param output_tokens [Integer, nil] Tokens in response
|
|
419
|
-
# @return [Integer] The created cache entry ID
|
|
420
369
|
def llm_cache_store(operation:, model:, input_hash:, result_json:, input_tokens: nil, output_tokens: nil)
|
|
421
370
|
cache_key = Digest::SHA256.hexdigest("#{operation}:#{model}:#{input_hash}")
|
|
422
371
|
|
|
@@ -439,83 +388,17 @@ module ClaudeMemory
|
|
|
439
388
|
)
|
|
440
389
|
end
|
|
441
390
|
|
|
442
|
-
# Generate a cache key for LLM response lookup
|
|
443
|
-
#
|
|
444
|
-
# @param operation [String] Operation type
|
|
445
|
-
# @param model [String] Model identifier
|
|
446
|
-
# @param input [String] Raw input content
|
|
447
|
-
# @return [String] SHA256 hex digest cache key
|
|
448
391
|
def llm_cache_key(operation, model, input)
|
|
449
392
|
input_hash = Digest::SHA256.hexdigest(input)
|
|
450
393
|
Digest::SHA256.hexdigest("#{operation}:#{model}:#{input_hash}")
|
|
451
394
|
end
|
|
452
395
|
|
|
453
|
-
# Prune cache entries older than the given age
|
|
454
|
-
#
|
|
455
|
-
# @param max_age_seconds [Integer] Maximum age in seconds (default: 7 days)
|
|
456
|
-
# @return [Integer] Number of entries pruned
|
|
457
396
|
def llm_cache_prune(max_age_seconds: 604_800)
|
|
458
397
|
cutoff = (Time.now - max_age_seconds).utc.iso8601
|
|
459
398
|
llm_cache.where { created_at < cutoff }.delete
|
|
460
399
|
end
|
|
461
400
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
def ensure_schema!
|
|
465
|
-
migrations_path = File.expand_path("../../../db/migrations", __dir__)
|
|
466
|
-
|
|
467
|
-
# Handle backward compatibility: databases created with old migration system
|
|
468
|
-
sync_legacy_schema_version!
|
|
469
|
-
|
|
470
|
-
# Skip migration if the database is already ahead of this gem's version.
|
|
471
|
-
# This happens when a newer gem version migrated the DB and an older
|
|
472
|
-
# installed gem (e.g. via hooks) tries to open it.
|
|
473
|
-
current = current_schema_version
|
|
474
|
-
return if current && current > SCHEMA_VERSION
|
|
475
|
-
|
|
476
|
-
# Run Sequel migrations to bring database to target version
|
|
477
|
-
Sequel::Migrator.run(@db, migrations_path, target: SCHEMA_VERSION)
|
|
478
|
-
|
|
479
|
-
# Set created_at timestamp on first initialization
|
|
480
|
-
set_meta("created_at", Time.now.utc.iso8601) unless get_meta("created_at")
|
|
481
|
-
|
|
482
|
-
# Sync legacy schema_version meta key with Sequel's schema_info
|
|
483
|
-
# This maintains backwards compatibility with code that reads schema_version
|
|
484
|
-
sequel_version = @db[:schema_info].get(:version) if @db.table_exists?(:schema_info)
|
|
485
|
-
set_meta("schema_version", sequel_version.to_s) if sequel_version
|
|
486
|
-
end
|
|
487
|
-
|
|
488
|
-
# Sync legacy schema_version from meta table to Sequel's schema_info
|
|
489
|
-
# Handles two cases:
|
|
490
|
-
# 1. No schema_info table exists (old system, pre-Sequel migrations)
|
|
491
|
-
# 2. schema_info exists but is out of sync with meta.schema_version
|
|
492
|
-
def sync_legacy_schema_version!
|
|
493
|
-
return unless @db.table_exists?(:meta)
|
|
494
|
-
|
|
495
|
-
meta_version = get_meta("schema_version")&.to_i
|
|
496
|
-
return unless meta_version && meta_version >= 2
|
|
497
|
-
|
|
498
|
-
# Verify database actually has v2+ schema (defensive check)
|
|
499
|
-
columns = @db.schema(:content_items).map(&:first) if @db.table_exists?(:content_items)
|
|
500
|
-
return unless columns&.include?(:project_path)
|
|
501
|
-
|
|
502
|
-
# Create or update schema_info to match meta.schema_version
|
|
503
|
-
@db.create_table?(:schema_info) do
|
|
504
|
-
Integer :version, null: false, default: 0
|
|
505
|
-
end
|
|
506
|
-
|
|
507
|
-
sequel_version = @db[:schema_info].get(:version)
|
|
508
|
-
if sequel_version.nil? || sequel_version < meta_version
|
|
509
|
-
# Update schema_info to match meta (old system's version)
|
|
510
|
-
@db[:schema_info].delete
|
|
511
|
-
@db[:schema_info].insert(version: meta_version)
|
|
512
|
-
end
|
|
513
|
-
end
|
|
514
|
-
|
|
515
|
-
def current_schema_version
|
|
516
|
-
return nil unless @db.table_exists?(:schema_info)
|
|
517
|
-
@db[:schema_info].get(:version)
|
|
518
|
-
end
|
|
401
|
+
# --- Meta ---
|
|
519
402
|
|
|
520
403
|
def set_meta(key, value)
|
|
521
404
|
@db[:meta].insert_conflict(target: :key, update: {value: value}).insert(key: key, value: value)
|
|
@@ -525,11 +408,12 @@ module ClaudeMemory
|
|
|
525
408
|
@db[:meta].where(key: key).get(:value)
|
|
526
409
|
end
|
|
527
410
|
|
|
411
|
+
private
|
|
412
|
+
|
|
528
413
|
def generate_docid(subject_entity_id, predicate, object_literal, created_at)
|
|
529
414
|
input = "#{subject_entity_id}:#{predicate}:#{object_literal}:#{created_at}"
|
|
530
415
|
docid = Digest::SHA256.hexdigest(input)[0, 8]
|
|
531
416
|
|
|
532
|
-
# Handle unlikely collisions by rehashing with a counter
|
|
533
417
|
counter = 0
|
|
534
418
|
while facts.where(docid: docid).any?
|
|
535
419
|
counter += 1
|
|
@@ -6,7 +6,20 @@
|
|
|
6
6
|
{
|
|
7
7
|
"type": "command",
|
|
8
8
|
"command": "claude-memory hook ingest",
|
|
9
|
-
"timeout": 10
|
|
9
|
+
"timeout": 10,
|
|
10
|
+
"statusMessage": "Saving memory..."
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"StopFailure": [
|
|
16
|
+
{
|
|
17
|
+
"hooks": [
|
|
18
|
+
{
|
|
19
|
+
"type": "command",
|
|
20
|
+
"command": "claude-memory hook ingest",
|
|
21
|
+
"timeout": 10,
|
|
22
|
+
"statusMessage": "Saving memory..."
|
|
10
23
|
}
|
|
11
24
|
]
|
|
12
25
|
}
|
|
@@ -17,7 +30,8 @@
|
|
|
17
30
|
{
|
|
18
31
|
"type": "command",
|
|
19
32
|
"command": "claude-memory hook ingest",
|
|
20
|
-
"timeout": 10
|
|
33
|
+
"timeout": 10,
|
|
34
|
+
"statusMessage": "Saving memory..."
|
|
21
35
|
}
|
|
22
36
|
]
|
|
23
37
|
}
|
|
@@ -28,12 +42,14 @@
|
|
|
28
42
|
{
|
|
29
43
|
"type": "command",
|
|
30
44
|
"command": "claude-memory hook ingest",
|
|
31
|
-
"timeout": 30
|
|
45
|
+
"timeout": 30,
|
|
46
|
+
"statusMessage": "Saving memory..."
|
|
32
47
|
},
|
|
33
48
|
{
|
|
34
49
|
"type": "command",
|
|
35
50
|
"command": "claude-memory hook sweep",
|
|
36
|
-
"timeout": 30
|
|
51
|
+
"timeout": 30,
|
|
52
|
+
"statusMessage": "Sweeping memory..."
|
|
37
53
|
}
|
|
38
54
|
]
|
|
39
55
|
}
|
|
@@ -44,12 +60,14 @@
|
|
|
44
60
|
{
|
|
45
61
|
"type": "command",
|
|
46
62
|
"command": "claude-memory hook ingest",
|
|
47
|
-
"timeout": 30
|
|
63
|
+
"timeout": 30,
|
|
64
|
+
"statusMessage": "Saving memory..."
|
|
48
65
|
},
|
|
49
66
|
{
|
|
50
67
|
"type": "command",
|
|
51
68
|
"command": "claude-memory hook sweep",
|
|
52
|
-
"timeout": 30
|
|
69
|
+
"timeout": 30,
|
|
70
|
+
"statusMessage": "Sweeping memory..."
|
|
53
71
|
}
|
|
54
72
|
]
|
|
55
73
|
}
|
|
@@ -61,7 +79,8 @@
|
|
|
61
79
|
{
|
|
62
80
|
"type": "command",
|
|
63
81
|
"command": "claude-memory hook sweep",
|
|
64
|
-
"timeout": 10
|
|
82
|
+
"timeout": 10,
|
|
83
|
+
"statusMessage": "Sweeping memory..."
|
|
65
84
|
}
|
|
66
85
|
]
|
|
67
86
|
}
|