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.
- checksums.yaml +4 -4
- data/.claude/memory.sqlite3 +0 -0
- data/.claude/rules/claude_memory.generated.md +32 -2
- data/.claude/settings.json +65 -15
- data/.claude/settings.local.json +5 -2
- data/.claude/skills/improve/SKILL.md +113 -25
- data/.claude/skills/upgrade-dependencies/SKILL.md +154 -0
- data/.claude-plugin/commands/distill-transcripts.md +98 -0
- data/.claude-plugin/commands/memory-recall.md +67 -0
- data/.claude-plugin/marketplace.json +2 -2
- data/.claude-plugin/plugin.json +3 -3
- data/.claude-plugin/scripts/hook-runner.sh +14 -0
- data/.claude-plugin/scripts/serve-mcp.sh +14 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +90 -1
- data/CLAUDE.md +56 -18
- data/README.md +35 -0
- data/db/migrations/013_add_mcp_tool_calls.rb +26 -0
- data/db/migrations/014_canonicalize_predicates.rb +30 -0
- data/docs/improvements.md +74 -74
- data/docs/influence/claude-mem.md +1 -0
- data/docs/influence/claude-supermemory.md +1 -0
- data/docs/influence/episodic-memory.md +1 -0
- data/docs/influence/grepai.md +1 -0
- data/docs/influence/kbs.md +1 -0
- data/docs/influence/lossless-claw.md +1 -0
- data/docs/influence/qmd.md +1 -0
- 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 +149 -0
- data/lib/claude_memory/commands/doctor_command.rb +2 -0
- data/lib/claude_memory/commands/embeddings_command.rb +198 -0
- data/lib/claude_memory/commands/help_command.rb +12 -1
- 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 +47 -32
- data/lib/claude_memory/commands/reject_command.rb +62 -0
- data/lib/claude_memory/commands/restore_command.rb +77 -0
- data/lib/claude_memory/commands/skills/distill-transcripts.md +102 -0
- data/lib/claude_memory/commands/skills/memory-recall.md +67 -0
- data/lib/claude_memory/commands/stats_command.rb +98 -2
- data/lib/claude_memory/configuration.rb +14 -1
- 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/distill/json_schema.md +8 -4
- data/lib/claude_memory/distill/null_distiller.rb +2 -0
- data/lib/claude_memory/domain/entity.rb +13 -1
- data/lib/claude_memory/domain/fact.rb +26 -2
- data/lib/claude_memory/domain/provenance.rb +0 -1
- data/lib/claude_memory/embeddings/api_adapter.rb +97 -0
- data/lib/claude_memory/embeddings/dimension_check.rb +23 -0
- data/lib/claude_memory/embeddings/fastembed_adapter.rb +46 -12
- data/lib/claude_memory/embeddings/generator.rb +4 -0
- data/lib/claude_memory/embeddings/inspector.rb +91 -0
- data/lib/claude_memory/embeddings/model_registry.rb +210 -0
- data/lib/claude_memory/embeddings/resolver.rb +44 -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/ingest/ingester.rb +17 -0
- data/lib/claude_memory/mcp/handlers/context_handlers.rb +38 -0
- data/lib/claude_memory/mcp/handlers/management_handlers.rb +169 -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 +205 -0
- data/lib/claude_memory/mcp/instructions_builder.rb +19 -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/server.rb +22 -1
- data/lib/claude_memory/mcp/telemetry.rb +86 -0
- data/lib/claude_memory/mcp/text_summary.rb +26 -0
- data/lib/claude_memory/mcp/tool_definitions.rb +116 -4
- data/lib/claude_memory/mcp/tool_helpers.rb +43 -0
- data/lib/claude_memory/mcp/tools.rb +50 -679
- data/lib/claude_memory/publish.rb +40 -5
- 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 +121 -673
- data/lib/claude_memory/resolve/predicate_policy.rb +63 -3
- data/lib/claude_memory/resolve/resolver.rb +43 -0
- 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 +334 -201
- data/lib/claude_memory/store/store_manager.rb +50 -1
- data/lib/claude_memory/sweep/maintenance.rb +115 -1
- data/lib/claude_memory/sweep/sweeper.rb +3 -0
- data/lib/claude_memory/templates/hooks.example.json +26 -7
- data/lib/claude_memory/version.rb +1 -1
- data/lib/claude_memory.rb +16 -0
- metadata +48 -8
- data/.claude/memory.sqlite3-shm +0 -0
- 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
|
-
"
|
|
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
|
-
"
|
|
12
|
-
"
|
|
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:
|
|
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 = 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
|