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.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/memory.sqlite3-shm +0 -0
  4. data/.claude/memory.sqlite3-wal +0 -0
  5. data/.claude/settings.json +78 -6
  6. data/.claude/settings.local.json +2 -1
  7. data/.claude/skills/improve/SKILL.md +113 -25
  8. data/.claude-plugin/commands/distill-transcripts.md +98 -0
  9. data/.claude-plugin/commands/memory-recall.md +67 -0
  10. data/.claude-plugin/marketplace.json +1 -1
  11. data/.claude-plugin/plugin.json +1 -1
  12. data/CHANGELOG.md +49 -1
  13. data/CLAUDE.md +29 -5
  14. data/docs/improvements.md +18 -56
  15. data/docs/quality_review.md +119 -224
  16. data/hooks/hooks.json +39 -7
  17. data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
  18. data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
  19. data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
  20. data/lib/claude_memory/commands/completion_command.rb +179 -0
  21. data/lib/claude_memory/commands/doctor_command.rb +2 -0
  22. data/lib/claude_memory/commands/help_command.rb +4 -0
  23. data/lib/claude_memory/commands/hook_command.rb +2 -1
  24. data/lib/claude_memory/commands/index_command.rb +85 -78
  25. data/lib/claude_memory/commands/initializers/database_ensurer.rb +16 -0
  26. data/lib/claude_memory/commands/initializers/global_initializer.rb +2 -1
  27. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +55 -11
  28. data/lib/claude_memory/commands/initializers/project_initializer.rb +2 -1
  29. data/lib/claude_memory/commands/install_skill_command.rb +78 -0
  30. data/lib/claude_memory/commands/registry.rb +3 -1
  31. data/lib/claude_memory/commands/skills/distill-transcripts.md +98 -0
  32. data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
  33. data/lib/claude_memory/core/fact_ranker.rb +2 -2
  34. data/lib/claude_memory/core/rr_fusion.rb +23 -6
  35. data/lib/claude_memory/core/snippet_extractor.rb +7 -3
  36. data/lib/claude_memory/core/text_builder.rb +11 -0
  37. data/lib/claude_memory/domain/provenance.rb +0 -1
  38. data/lib/claude_memory/embeddings/api_adapter.rb +96 -0
  39. data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
  40. data/lib/claude_memory/embeddings/fastembed_adapter.rb +4 -0
  41. data/lib/claude_memory/embeddings/generator.rb +4 -0
  42. data/lib/claude_memory/embeddings/resolver.rb +18 -0
  43. data/lib/claude_memory/hook/context_injector.rb +58 -2
  44. data/lib/claude_memory/hook/distillation_runner.rb +46 -0
  45. data/lib/claude_memory/hook/handler.rb +11 -2
  46. data/lib/claude_memory/index/vector_index.rb +15 -2
  47. data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
  48. data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
  49. data/lib/claude_memory/mcp/handlers/management_handlers.rb +145 -0
  50. data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
  51. data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
  52. data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
  53. data/lib/claude_memory/mcp/handlers/stats_handlers.rb +202 -0
  54. data/lib/claude_memory/mcp/instructions_builder.rb +2 -1
  55. data/lib/claude_memory/mcp/query_guide.rb +10 -0
  56. data/lib/claude_memory/mcp/response_formatter.rb +1 -0
  57. data/lib/claude_memory/mcp/text_summary.rb +26 -0
  58. data/lib/claude_memory/mcp/tool_definitions.rb +30 -1
  59. data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
  60. data/lib/claude_memory/mcp/tools.rb +39 -678
  61. data/lib/claude_memory/recall/dual_engine.rb +105 -0
  62. data/lib/claude_memory/recall/legacy_engine.rb +138 -0
  63. data/lib/claude_memory/recall/query_core.rb +371 -0
  64. data/lib/claude_memory/recall.rb +29 -662
  65. data/lib/claude_memory/shortcuts.rb +4 -4
  66. data/lib/claude_memory/store/retry_handler.rb +61 -0
  67. data/lib/claude_memory/store/schema_manager.rb +68 -0
  68. data/lib/claude_memory/store/sqlite_store.rb +85 -201
  69. data/lib/claude_memory/templates/hooks.example.json +26 -7
  70. data/lib/claude_memory/version.rb +1 -1
  71. data/lib/claude_memory.rb +11 -0
  72. metadata +23 -1
@@ -5,22 +5,22 @@ module ClaudeMemory
5
5
  QUERIES = {
6
6
  decisions: {
7
7
  query: "decision constraint rule requirement",
8
- scope: :all,
8
+ scope: "all",
9
9
  limit: 10
10
10
  },
11
11
  architecture: {
12
12
  query: "uses framework implements architecture pattern",
13
- scope: :all,
13
+ scope: "all",
14
14
  limit: 10
15
15
  },
16
16
  conventions: {
17
17
  query: "convention style format pattern prefer",
18
- scope: :global,
18
+ scope: "global",
19
19
  limit: 20
20
20
  },
21
21
  project_config: {
22
22
  query: "uses requires depends_on configuration",
23
- scope: :project,
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
- SCHEMA_VERSION = 12
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
- def content_items
107
- @db[:content_items]
108
- end
44
+ # --- Table accessors ---
109
45
 
110
- def delta_cursors
111
- @db[:delta_cursors]
112
- end
46
+ def content_items = @db[:content_items]
113
47
 
114
- def entities
115
- @db[:entities]
116
- end
48
+ def delta_cursors = @db[:delta_cursors]
117
49
 
118
- def entity_aliases
119
- @db[:entity_aliases]
120
- end
50
+ def entities = @db[:entities]
121
51
 
122
- def facts
123
- @db[:facts]
124
- end
52
+ def entity_aliases = @db[:entity_aliases]
125
53
 
126
- def provenance
127
- @db[:provenance]
128
- end
54
+ def facts = @db[:facts]
129
55
 
130
- def fact_links
131
- @db[:fact_links]
132
- end
56
+ def provenance = @db[:provenance]
133
57
 
134
- def conflicts
135
- @db[:conflicts]
136
- end
58
+ def fact_links = @db[:fact_links]
137
59
 
138
- def tool_calls
139
- @db[:tool_calls]
140
- end
60
+ def conflicts = @db[:conflicts]
141
61
 
142
- def operation_progress
143
- @db[:operation_progress]
144
- end
62
+ def tool_calls = @db[:tool_calls]
145
63
 
146
- def schema_health
147
- @db[:schema_health]
148
- end
64
+ def operation_progress = @db[:operation_progress]
149
65
 
150
- def ingestion_metrics
151
- @db[:ingestion_metrics]
152
- end
66
+ def schema_health = @db[:schema_health]
153
67
 
154
- def llm_cache
155
- @db[:llm_cache]
156
- end
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
- # Record token usage metrics for a distillation operation
347
- #
348
- # @param content_item_id [Integer] The content item that was distilled
349
- # @param input_tokens [Integer] Tokens sent to the API
350
- # @param output_tokens [Integer] Tokens returned from the API
351
- # @param facts_extracted [Integer] Number of facts extracted
352
- # @return [Integer] The created metric record ID
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
- # Look up a cached LLM response by cache key
404
- #
405
- # @param cache_key [String] SHA256 hex digest of operation+model+input
406
- # @return [Hash, nil] Cached result row or nil
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
- private
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
  }
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeMemory
4
- VERSION = "0.7.1"
4
+ VERSION = "0.8.0"
5
5
  end