claude_memory 0.8.0 → 0.9.1
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 +94 -2
- data/.claude/settings.json +30 -52
- data/.claude/settings.local.json +3 -1
- data/.claude/skills/release/SKILL.md +168 -0
- data/.claude/skills/upgrade-dependencies/SKILL.md +154 -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 +47 -0
- data/CLAUDE.md +31 -17
- 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 +58 -20
- 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/lib/claude_memory/commands/completion_command.rb +1 -31
- data/lib/claude_memory/commands/embeddings_command.rb +198 -0
- data/lib/claude_memory/commands/help_command.rb +8 -1
- data/lib/claude_memory/commands/registry.rb +47 -34
- 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 +5 -1
- data/lib/claude_memory/commands/stats_command.rb +98 -2
- data/lib/claude_memory/configuration.rb +14 -1
- 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/embeddings/api_adapter.rb +5 -4
- data/lib/claude_memory/embeddings/fastembed_adapter.rb +43 -13
- 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 +32 -6
- data/lib/claude_memory/ingest/ingester.rb +17 -0
- data/lib/claude_memory/mcp/handlers/management_handlers.rb +24 -0
- data/lib/claude_memory/mcp/handlers/stats_handlers.rb +5 -2
- data/lib/claude_memory/mcp/instructions_builder.rb +17 -0
- data/lib/claude_memory/mcp/server.rb +30 -3
- data/lib/claude_memory/mcp/telemetry.rb +86 -0
- data/lib/claude_memory/mcp/tool_definitions.rb +86 -3
- data/lib/claude_memory/mcp/tools.rb +10 -0
- data/lib/claude_memory/publish.rb +40 -5
- data/lib/claude_memory/recall.rb +81 -0
- data/lib/claude_memory/resolve/predicate_policy.rb +63 -3
- data/lib/claude_memory/resolve/resolver.rb +43 -0
- data/lib/claude_memory/store/schema_manager.rb +1 -1
- data/lib/claude_memory/store/sqlite_store.rb +250 -1
- 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/version.rb +1 -1
- data/lib/claude_memory.rb +5 -0
- metadata +27 -8
- data/.claude/memory.sqlite3-shm +0 -0
- data/.claude/memory.sqlite3-wal +0 -0
data/lib/claude_memory/recall.rb
CHANGED
|
@@ -1,29 +1,51 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ClaudeMemory
|
|
4
|
+
# Query interface for facts across dual databases (global + project).
|
|
5
|
+
# Delegates to DualEngine or LegacyEngine depending on the store type.
|
|
4
6
|
class Recall
|
|
7
|
+
# @return [String] query only project-scoped facts
|
|
5
8
|
SCOPE_PROJECT = "project"
|
|
9
|
+
# @return [String] query only global-scoped facts
|
|
6
10
|
SCOPE_GLOBAL = "global"
|
|
11
|
+
# @return [String] query both project and global facts (default)
|
|
7
12
|
SCOPE_ALL = "all"
|
|
8
13
|
|
|
9
14
|
class << self
|
|
15
|
+
# @param manager [Store::StoreManager] dual-database manager
|
|
16
|
+
# @param limit [Integer] max results
|
|
17
|
+
# @return [Array<Hash>] recent decision facts
|
|
10
18
|
def recent_decisions(manager, limit: 10)
|
|
11
19
|
Shortcuts.for(:decisions, manager, limit: limit)
|
|
12
20
|
end
|
|
13
21
|
|
|
22
|
+
# @param manager [Store::StoreManager] dual-database manager
|
|
23
|
+
# @param limit [Integer] max results
|
|
24
|
+
# @return [Array<Hash>] architecture-related facts
|
|
14
25
|
def architecture_choices(manager, limit: 10)
|
|
15
26
|
Shortcuts.for(:architecture, manager, limit: limit)
|
|
16
27
|
end
|
|
17
28
|
|
|
29
|
+
# @param manager [Store::StoreManager] dual-database manager
|
|
30
|
+
# @param limit [Integer] max results
|
|
31
|
+
# @return [Array<Hash>] convention facts
|
|
18
32
|
def conventions(manager, limit: 20)
|
|
19
33
|
Shortcuts.for(:conventions, manager, limit: limit)
|
|
20
34
|
end
|
|
21
35
|
|
|
36
|
+
# @param manager [Store::StoreManager] dual-database manager
|
|
37
|
+
# @param limit [Integer] max results
|
|
38
|
+
# @return [Array<Hash>] project configuration facts
|
|
22
39
|
def project_config(manager, limit: 10)
|
|
23
40
|
Shortcuts.for(:project_config, manager, limit: limit)
|
|
24
41
|
end
|
|
25
42
|
end
|
|
26
43
|
|
|
44
|
+
# @param store_or_manager [Store::SQLiteStore, Store::StoreManager] database store or dual-database manager
|
|
45
|
+
# @param fts [Index::LexicalFTS, nil] full-text search index (used only with legacy single-store)
|
|
46
|
+
# @param project_path [String, nil] project root path (defaults to Configuration#project_dir)
|
|
47
|
+
# @param env [Hash] environment variables
|
|
48
|
+
# @param embedding_generator [Object, nil] vector embedding generator for semantic search
|
|
27
49
|
def initialize(store_or_manager, fts: nil, project_path: nil, env: ENV, embedding_generator: nil)
|
|
28
50
|
config = Configuration.new(env)
|
|
29
51
|
resolved_project_path = project_path || config.project_dir
|
|
@@ -45,46 +67,105 @@ module ClaudeMemory
|
|
|
45
67
|
end
|
|
46
68
|
end
|
|
47
69
|
|
|
70
|
+
# Search facts by text query using FTS5
|
|
71
|
+
# @param query_text [String] search terms
|
|
72
|
+
# @param limit [Integer] max results
|
|
73
|
+
# @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
|
|
74
|
+
# @param include_raw_text [Boolean] include source content text in results
|
|
75
|
+
# @param intent [String, nil] query intent hint for ranking
|
|
76
|
+
# @return [Array<Hash>] matching facts with provenance
|
|
48
77
|
def query(query_text, limit: 10, scope: SCOPE_ALL, include_raw_text: false, intent: nil)
|
|
49
78
|
@engine.query(query_text, limit: limit, scope: scope, include_raw_text: include_raw_text, intent: intent)
|
|
50
79
|
end
|
|
51
80
|
|
|
81
|
+
# Search content items (not facts) via FTS5 index
|
|
82
|
+
# @param query_text [String] search terms
|
|
83
|
+
# @param limit [Integer] max results
|
|
84
|
+
# @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
|
|
85
|
+
# @param intent [String, nil] query intent hint for ranking
|
|
86
|
+
# @return [Array<Hash>] matching content items
|
|
52
87
|
def query_index(query_text, limit: 20, scope: SCOPE_ALL, intent: nil)
|
|
53
88
|
@engine.query_index(query_text, limit: limit, scope: scope, intent: intent)
|
|
54
89
|
end
|
|
55
90
|
|
|
91
|
+
# Traverse fact relationships (supersessions, conflicts) as a graph
|
|
92
|
+
# @param fact_id [Integer] starting fact ID
|
|
93
|
+
# @param depth [Integer] traversal depth
|
|
94
|
+
# @param scope [String, nil] optional scope filter
|
|
95
|
+
# @return [Hash] graph with nodes and edges
|
|
56
96
|
def fact_graph(fact_id, depth: 2, scope: nil)
|
|
57
97
|
@engine.fact_graph(fact_id, depth: depth, scope: scope)
|
|
58
98
|
end
|
|
59
99
|
|
|
100
|
+
# Show provenance chain for a fact
|
|
101
|
+
# @param fact_id_or_docid [Integer, String] fact ID or document ID
|
|
102
|
+
# @param scope [String, nil] optional scope filter
|
|
103
|
+
# @return [Hash] provenance details including source content
|
|
60
104
|
def explain(fact_id_or_docid, scope: nil)
|
|
61
105
|
@engine.explain(fact_id_or_docid, scope: scope)
|
|
62
106
|
end
|
|
63
107
|
|
|
108
|
+
# List facts created or modified since a given time
|
|
109
|
+
# @param since [String] ISO 8601 timestamp
|
|
110
|
+
# @param limit [Integer] max results
|
|
111
|
+
# @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
|
|
112
|
+
# @return [Array<Hash>] recently changed facts
|
|
64
113
|
def changes(since:, limit: 50, scope: SCOPE_ALL)
|
|
65
114
|
@engine.changes(since: since, limit: limit, scope: scope)
|
|
66
115
|
end
|
|
67
116
|
|
|
117
|
+
# List open fact conflicts
|
|
118
|
+
# @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
|
|
119
|
+
# @return [Array<Hash>] unresolved conflicts
|
|
68
120
|
def conflicts(scope: SCOPE_ALL)
|
|
69
121
|
@engine.conflicts(scope: scope)
|
|
70
122
|
end
|
|
71
123
|
|
|
124
|
+
# Find facts associated with a git branch
|
|
125
|
+
# @param branch_name [String] git branch name
|
|
126
|
+
# @param limit [Integer] max results
|
|
127
|
+
# @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
|
|
128
|
+
# @return [Array<Hash>] facts from the given branch
|
|
72
129
|
def facts_by_branch(branch_name, limit: 20, scope: SCOPE_ALL)
|
|
73
130
|
@engine.facts_by_branch(branch_name, limit: limit, scope: scope)
|
|
74
131
|
end
|
|
75
132
|
|
|
133
|
+
# Find facts associated with a working directory
|
|
134
|
+
# @param cwd [String] directory path
|
|
135
|
+
# @param limit [Integer] max results
|
|
136
|
+
# @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
|
|
137
|
+
# @return [Array<Hash>] facts from the given directory
|
|
76
138
|
def facts_by_directory(cwd, limit: 20, scope: SCOPE_ALL)
|
|
77
139
|
@engine.facts_by_directory(cwd, limit: limit, scope: scope)
|
|
78
140
|
end
|
|
79
141
|
|
|
142
|
+
# Find facts associated with a specific tool
|
|
143
|
+
# @param tool_name [String] tool name (e.g., "Read", "Bash")
|
|
144
|
+
# @param limit [Integer] max results
|
|
145
|
+
# @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
|
|
146
|
+
# @return [Array<Hash>] facts from sessions using the given tool
|
|
80
147
|
def facts_by_tool(tool_name, limit: 20, scope: SCOPE_ALL)
|
|
81
148
|
@engine.facts_by_tool(tool_name, limit: limit, scope: scope)
|
|
82
149
|
end
|
|
83
150
|
|
|
151
|
+
# Search facts using vector embeddings (semantic similarity)
|
|
152
|
+
# @param text [String] natural language query
|
|
153
|
+
# @param limit [Integer] max results
|
|
154
|
+
# @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
|
|
155
|
+
# @param mode [Symbol] :vector, :lexical, or :both (hybrid RRF)
|
|
156
|
+
# @param explain [Boolean] include scoring breakdown in results
|
|
157
|
+
# @param intent [String, nil] query intent hint for ranking
|
|
158
|
+
# @return [Array<Hash>] semantically similar facts
|
|
84
159
|
def query_semantic(text, limit: 10, scope: SCOPE_ALL, mode: :both, explain: false, intent: nil)
|
|
85
160
|
@engine.query_semantic(text, limit: limit, scope: scope, mode: mode, explain: explain, intent: intent)
|
|
86
161
|
end
|
|
87
162
|
|
|
163
|
+
# Find facts at the intersection of multiple concepts
|
|
164
|
+
# @param concepts [Array<String>] 2-5 concept terms to intersect
|
|
165
|
+
# @param limit [Integer] max results
|
|
166
|
+
# @param scope [String] one of SCOPE_ALL, SCOPE_PROJECT, SCOPE_GLOBAL
|
|
167
|
+
# @return [Array<Hash>] facts matching all given concepts
|
|
168
|
+
# @raise [ArgumentError] if concepts count is not 2-5
|
|
88
169
|
def query_concepts(concepts, limit: 10, scope: SCOPE_ALL)
|
|
89
170
|
raise ArgumentError, "Must provide 2-5 concepts" unless (2..5).cover?(concepts.size)
|
|
90
171
|
|
|
@@ -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])
|