claude_memory 0.7.1 → 0.9.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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/memory.sqlite3 +0 -0
  3. data/.claude/rules/claude_memory.generated.md +32 -2
  4. data/.claude/settings.json +65 -15
  5. data/.claude/settings.local.json +5 -2
  6. data/.claude/skills/improve/SKILL.md +113 -25
  7. data/.claude/skills/upgrade-dependencies/SKILL.md +154 -0
  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 +2 -2
  11. data/.claude-plugin/plugin.json +3 -3
  12. data/.claude-plugin/scripts/hook-runner.sh +14 -0
  13. data/.claude-plugin/scripts/serve-mcp.sh +14 -0
  14. data/.ruby-version +1 -1
  15. data/CHANGELOG.md +90 -1
  16. data/CLAUDE.md +56 -18
  17. data/README.md +35 -0
  18. data/db/migrations/013_add_mcp_tool_calls.rb +26 -0
  19. data/db/migrations/014_canonicalize_predicates.rb +30 -0
  20. data/docs/improvements.md +74 -74
  21. data/docs/influence/claude-mem.md +1 -0
  22. data/docs/influence/claude-supermemory.md +1 -0
  23. data/docs/influence/episodic-memory.md +1 -0
  24. data/docs/influence/grepai.md +1 -0
  25. data/docs/influence/kbs.md +1 -0
  26. data/docs/influence/lossless-claw.md +1 -0
  27. data/docs/influence/qmd.md +1 -0
  28. data/docs/quality_review.md +119 -224
  29. data/hooks/hooks.json +39 -7
  30. data/lib/claude_memory/commands/checks/distill_check.rb +61 -0
  31. data/lib/claude_memory/commands/checks/hooks_check.rb +2 -2
  32. data/lib/claude_memory/commands/checks/vec_check.rb +2 -1
  33. data/lib/claude_memory/commands/completion_command.rb +149 -0
  34. data/lib/claude_memory/commands/doctor_command.rb +2 -0
  35. data/lib/claude_memory/commands/embeddings_command.rb +198 -0
  36. data/lib/claude_memory/commands/help_command.rb +12 -1
  37. data/lib/claude_memory/commands/hook_command.rb +2 -1
  38. data/lib/claude_memory/commands/index_command.rb +85 -78
  39. data/lib/claude_memory/commands/initializers/database_ensurer.rb +16 -0
  40. data/lib/claude_memory/commands/initializers/global_initializer.rb +2 -1
  41. data/lib/claude_memory/commands/initializers/hooks_configurator.rb +55 -11
  42. data/lib/claude_memory/commands/initializers/project_initializer.rb +2 -1
  43. data/lib/claude_memory/commands/install_skill_command.rb +78 -0
  44. data/lib/claude_memory/commands/registry.rb +47 -32
  45. data/lib/claude_memory/commands/reject_command.rb +62 -0
  46. data/lib/claude_memory/commands/restore_command.rb +77 -0
  47. data/lib/claude_memory/commands/skills/distill-transcripts.md +102 -0
  48. data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
  49. data/lib/claude_memory/commands/stats_command.rb +98 -2
  50. data/lib/claude_memory/configuration.rb +14 -1
  51. data/lib/claude_memory/core/fact_ranker.rb +2 -2
  52. data/lib/claude_memory/core/rr_fusion.rb +23 -6
  53. data/lib/claude_memory/core/snippet_extractor.rb +7 -3
  54. data/lib/claude_memory/core/text_builder.rb +11 -0
  55. data/lib/claude_memory/distill/json_schema.md +8 -4
  56. data/lib/claude_memory/distill/null_distiller.rb +2 -0
  57. data/lib/claude_memory/domain/entity.rb +13 -1
  58. data/lib/claude_memory/domain/fact.rb +26 -2
  59. data/lib/claude_memory/domain/provenance.rb +0 -1
  60. data/lib/claude_memory/embeddings/api_adapter.rb +97 -0
  61. data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
  62. data/lib/claude_memory/embeddings/fastembed_adapter.rb +46 -12
  63. data/lib/claude_memory/embeddings/generator.rb +4 -0
  64. data/lib/claude_memory/embeddings/inspector.rb +91 -0
  65. data/lib/claude_memory/embeddings/model_registry.rb +210 -0
  66. data/lib/claude_memory/embeddings/resolver.rb +44 -0
  67. data/lib/claude_memory/hook/context_injector.rb +58 -2
  68. data/lib/claude_memory/hook/distillation_runner.rb +46 -0
  69. data/lib/claude_memory/hook/handler.rb +11 -2
  70. data/lib/claude_memory/index/vector_index.rb +15 -2
  71. data/lib/claude_memory/infrastructure/schema_validator.rb +3 -3
  72. data/lib/claude_memory/ingest/ingester.rb +17 -0
  73. data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
  74. data/lib/claude_memory/mcp/handlers/management_handlers.rb +169 -0
  75. data/lib/claude_memory/mcp/handlers/query_handlers.rb +115 -0
  76. data/lib/claude_memory/mcp/handlers/setup_handlers.rb +211 -0
  77. data/lib/claude_memory/mcp/handlers/shortcut_handlers.rb +37 -0
  78. data/lib/claude_memory/mcp/handlers/stats_handlers.rb +205 -0
  79. data/lib/claude_memory/mcp/instructions_builder.rb +19 -1
  80. data/lib/claude_memory/mcp/query_guide.rb +10 -0
  81. data/lib/claude_memory/mcp/response_formatter.rb +1 -0
  82. data/lib/claude_memory/mcp/server.rb +22 -1
  83. data/lib/claude_memory/mcp/telemetry.rb +86 -0
  84. data/lib/claude_memory/mcp/text_summary.rb +26 -0
  85. data/lib/claude_memory/mcp/tool_definitions.rb +116 -4
  86. data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
  87. data/lib/claude_memory/mcp/tools.rb +50 -679
  88. data/lib/claude_memory/publish.rb +40 -5
  89. data/lib/claude_memory/recall/dual_engine.rb +105 -0
  90. data/lib/claude_memory/recall/legacy_engine.rb +138 -0
  91. data/lib/claude_memory/recall/query_core.rb +371 -0
  92. data/lib/claude_memory/recall.rb +121 -673
  93. data/lib/claude_memory/resolve/predicate_policy.rb +63 -3
  94. data/lib/claude_memory/resolve/resolver.rb +43 -0
  95. data/lib/claude_memory/shortcuts.rb +4 -4
  96. data/lib/claude_memory/store/retry_handler.rb +61 -0
  97. data/lib/claude_memory/store/schema_manager.rb +68 -0
  98. data/lib/claude_memory/store/sqlite_store.rb +334 -201
  99. data/lib/claude_memory/store/store_manager.rb +50 -1
  100. data/lib/claude_memory/sweep/maintenance.rb +115 -1
  101. data/lib/claude_memory/sweep/sweeper.rb +3 -0
  102. data/lib/claude_memory/templates/hooks.example.json +26 -7
  103. data/lib/claude_memory/version.rb +1 -1
  104. data/lib/claude_memory.rb +16 -0
  105. metadata +48 -8
  106. data/.claude/memory.sqlite3-shm +0 -0
  107. data/.claude/memory.sqlite3-wal +0 -0
@@ -3,17 +3,77 @@
3
3
  module ClaudeMemory
4
4
  module Resolve
5
5
  class PredicatePolicy
6
+ # Canonical predicate vocabulary. Curated after a multi-project survey
7
+ # of real memory databases under ~/src — predicates with zero facts
8
+ # across every database were pruned; predicates observed in the wild
9
+ # but missing from the policy (architecture, uses_language) were added.
10
+ #
11
+ # - convention / decision: workhorse multi-value predicates
12
+ # - uses_framework / uses_language: multi-value (projects use multiple)
13
+ # - uses_database / deployment_platform / auth_method: single-value,
14
+ # correctly 1:1 per project in observed data
15
+ # - architecture: multi-value structural knowledge (was implicit)
6
16
  POLICIES = {
7
17
  "convention" => {cardinality: :multi, exclusive: false},
8
18
  "decision" => {cardinality: :multi, exclusive: false},
9
- "auth_method" => {cardinality: :single, exclusive: true},
19
+ "architecture" => {cardinality: :multi, exclusive: false},
20
+ "uses_framework" => {cardinality: :multi, exclusive: false},
21
+ "uses_language" => {cardinality: :multi, exclusive: false},
10
22
  "uses_database" => {cardinality: :single, exclusive: true},
11
- "uses_framework" => {cardinality: :single, exclusive: true},
12
- "deployment_platform" => {cardinality: :single, exclusive: true}
23
+ "deployment_platform" => {cardinality: :single, exclusive: true},
24
+ "auth_method" => {cardinality: :single, exclusive: true}
13
25
  }.freeze
14
26
 
15
27
  DEFAULT_POLICY = {cardinality: :multi, exclusive: false}.freeze
16
28
 
29
+ # Drift canonicalization. Maps predicate names the distiller has
30
+ # organically coined onto the canonical form in POLICIES. Consulted
31
+ # at insert time by the Resolver so synonym drift never fragments
32
+ # the knowledge graph.
33
+ #
34
+ # Entries observed in real project DBs:
35
+ # - has_convention (chaos_to_the_rescue): prefix-drift of convention
36
+ # - primary_language (prior policy entry): supplanted by uses_language
37
+ # which the distiller emits naturally and has multi-value semantics
38
+ SYNONYMS = {
39
+ "has_convention" => "convention",
40
+ "primary_language" => "uses_language"
41
+ }.freeze
42
+
43
+ # Section classification for the published snapshot. Keeps Publish
44
+ # from hard-coding predicate names; adding a new predicate to the
45
+ # policy and the section map in one place updates everything.
46
+ SECTION_MAP = {
47
+ "decision" => :decisions,
48
+ "convention" => :conventions,
49
+ "uses_database" => :constraints,
50
+ "uses_framework" => :constraints,
51
+ "uses_language" => :constraints,
52
+ "deployment_platform" => :constraints,
53
+ "auth_method" => :constraints
54
+ # architecture intentionally falls through to :additional for now
55
+ }.freeze
56
+
57
+ def self.known_predicates
58
+ POLICIES.keys
59
+ end
60
+
61
+ # Return the canonical form of a predicate name, applying known
62
+ # synonym mappings. Leaves unmapped predicates unchanged.
63
+ def self.canonicalize(predicate)
64
+ return predicate if predicate.nil?
65
+ SYNONYMS.fetch(predicate, predicate)
66
+ end
67
+
68
+ # Return the snapshot section a predicate belongs to.
69
+ # Respects legacy prefix/suffix patterns (decided_*, *_convention)
70
+ # that pre-date the policy.
71
+ def self.section_for(predicate)
72
+ return :decisions if predicate&.start_with?("decided_")
73
+ return :conventions if predicate&.include?("_convention")
74
+ SECTION_MAP.fetch(predicate, :additional)
75
+ end
76
+
17
77
  def self.policy_for(predicate)
18
78
  POLICIES.fetch(predicate, DEFAULT_POLICY)
19
79
  end
@@ -2,11 +2,32 @@
2
2
 
3
3
  module ClaudeMemory
4
4
  module Resolve
5
+ # Truth maintenance engine that processes distilled extractions into stored facts.
6
+ # Wraps entity resolution, fact insertion, supersession, and conflict detection
7
+ # in a single database transaction.
8
+ #
9
+ # @example
10
+ # resolver = Resolver.new(store)
11
+ # result = resolver.apply(extraction, content_item_id: 42, scope: "project")
12
+ # result[:facts_created] #=> 3
13
+ # result[:facts_superseded] #=> 1
5
14
  class Resolver
15
+ # @param store [Store::SQLiteStore] backing database for reads and writes
6
16
  def initialize(store)
7
17
  @store = store
8
18
  end
9
19
 
20
+ # Apply a distilled extraction, resolving each fact against existing knowledge.
21
+ # Facts may be inserted, reinforce an existing fact, supersede old facts, or
22
+ # create a conflict when the resolution is ambiguous.
23
+ #
24
+ # @param extraction [#entities, #facts] distilled extraction with entities and facts
25
+ # @param content_item_id [Integer, nil] source content item for provenance
26
+ # @param occurred_at [String, nil] ISO 8601 timestamp (defaults to now)
27
+ # @param project_path [String, nil] project path for scoped facts
28
+ # @param scope [String] default scope for facts ("project" or "global")
29
+ # @return [Hash] counts keyed by :entities_created, :facts_created,
30
+ # :facts_superseded, :conflicts_created, :provenance_created
10
31
  def apply(extraction, content_item_id: nil, occurred_at: nil, project_path: nil, scope: "project")
11
32
  occurred_at ||= Time.now.utc.iso8601
12
33
 
@@ -49,6 +70,21 @@ module ClaudeMemory
49
70
  end
50
71
 
51
72
  def resolve_fact(fact_data, entity_ids, content_item_id, occurred_at, project_path:, scope:)
73
+ # Canonicalize drift-prone predicate synonyms (has_convention →
74
+ # convention, primary_language → uses_language) before anything
75
+ # else looks at the predicate.
76
+ original_predicate = fact_data[:predicate]
77
+ canonical = PredicatePolicy.canonicalize(original_predicate)
78
+ if canonical != original_predicate
79
+ ClaudeMemory.logger.debug("resolve",
80
+ message: "Canonicalized predicate",
81
+ from: original_predicate,
82
+ to: canonical)
83
+ fact_data = fact_data.merge(predicate: canonical)
84
+ end
85
+
86
+ log_novel_predicate(canonical) unless PredicatePolicy.known_predicates.include?(canonical)
87
+
52
88
  subject_id = resolve_subject(fact_data, entity_ids)
53
89
  existing_facts = @store.facts_for_slot(subject_id, fact_data[:predicate])
54
90
  resolution = determine_resolution(existing_facts, fact_data, entity_ids)
@@ -57,6 +93,13 @@ module ClaudeMemory
57
93
  project_path: project_path, scope: scope)
58
94
  end
59
95
 
96
+ def log_novel_predicate(predicate)
97
+ ClaudeMemory.logger.warn("resolve",
98
+ message: "Novel predicate encountered",
99
+ predicate: predicate,
100
+ hint: "add to PredicatePolicy::POLICIES or PredicatePolicy::SYNONYMS to canonicalize")
101
+ end
102
+
60
103
  def resolve_subject(fact_data, entity_ids)
61
104
  entity_ids[fact_data[:subject]] ||
62
105
  @store.find_or_create_entity(type: "repo", name: fact_data[:subject])
@@ -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 = 14
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