claude_memory 0.7.0 → 0.7.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.
@@ -10,6 +10,10 @@ module ClaudeMemory
10
10
  default_budget_seconds: 5
11
11
  }.freeze
12
12
 
13
+ # Three-level escalation ensures sweep always makes progress.
14
+ # Source: lossless-claw three-level escalation pattern
15
+ ESCALATION_LEVELS = %i[normal aggressive fallback].freeze
16
+
13
17
  def initialize(store, config: {})
14
18
  @store = store
15
19
  @config = DEFAULT_CONFIG.merge(config)
@@ -24,16 +28,19 @@ module ClaudeMemory
24
28
  proposed_facts_expired: 0,
25
29
  disputed_facts_expired: 0,
26
30
  orphaned_provenance_deleted: 0,
27
- old_content_pruned: 0
31
+ old_content_pruned: 0,
32
+ escalation_level: :normal
28
33
  }
29
34
 
30
- expire_proposed_facts if within_budget?
31
- expire_disputed_facts if within_budget?
32
- prune_orphaned_provenance if within_budget?
33
- prune_old_content if within_budget?
34
- backfill_vec_index if within_budget?
35
- cleanup_vec_expired if within_budget?
36
- checkpoint_wal if within_budget?
35
+ maintenance = build_maintenance(:normal)
36
+
37
+ run_if_within_budget { @stats[:proposed_facts_expired] = maintenance.expire_proposed_facts }
38
+ run_if_within_budget { @stats[:disputed_facts_expired] = maintenance.expire_disputed_facts }
39
+ run_if_within_budget { @stats[:orphaned_provenance_deleted] = maintenance.prune_orphaned_provenance }
40
+ run_if_within_budget { @stats[:old_content_pruned] = maintenance.prune_old_content }
41
+ run_if_within_budget { @stats[:vec_backfilled] = maintenance.backfill_vec_index }
42
+ run_if_within_budget { @stats[:vec_cleaned] = maintenance.cleanup_vec_expired }
43
+ run_if_within_budget { @stats[:wal_checkpointed] = maintenance.checkpoint_wal }
37
44
 
38
45
  @stats[:elapsed_seconds] = Time.now - @start_time
39
46
  @stats[:budget_honored] = @stats[:elapsed_seconds] <= budget
@@ -41,90 +48,89 @@ module ClaudeMemory
41
48
  message: "Sweep complete",
42
49
  elapsed_seconds: @stats[:elapsed_seconds].round(3),
43
50
  budget_honored: @stats[:budget_honored],
51
+ escalation_level: @stats[:escalation_level],
44
52
  proposed_expired: @stats[:proposed_facts_expired],
45
53
  disputed_expired: @stats[:disputed_facts_expired])
46
54
  @stats
47
55
  end
48
56
 
49
- private
50
-
51
- def within_budget?
52
- budget = @config[:default_budget_seconds]
53
- (Time.now - @start_time) < budget
54
- end
57
+ # Run sweep with escalation: if normal sweep makes no progress,
58
+ # escalate to aggressive (halved TTLs), then fallback (force-expire oldest).
59
+ # Returns stats hash with :escalation_level indicating which level succeeded.
60
+ #
61
+ # Source: lossless-claw three-level escalation pattern
62
+ def run_with_escalation!(budget_seconds: nil)
63
+ stats = run!(budget_seconds: budget_seconds)
64
+ total = stats[:proposed_facts_expired] + stats[:disputed_facts_expired] +
65
+ stats[:orphaned_provenance_deleted] + stats[:old_content_pruned]
66
+
67
+ if total == 0
68
+ # Escalate to aggressive — halved TTLs
69
+ aggressive_maintenance = build_maintenance(:aggressive)
70
+ added = 0
71
+ added += aggressive_maintenance.expire_proposed_facts
72
+ added += aggressive_maintenance.expire_disputed_facts
73
+ added += aggressive_maintenance.prune_old_content
74
+
75
+ if added > 0
76
+ stats[:proposed_facts_expired] += added
77
+ stats[:escalation_level] = :aggressive
78
+ else
79
+ # Fallback — force-expire oldest proposed/disputed facts
80
+ fallback_count = force_expire_oldest
81
+ stats[:proposed_facts_expired] += fallback_count
82
+ stats[:escalation_level] = :fallback if fallback_count > 0
83
+ end
84
+
85
+ stats[:elapsed_seconds] = Time.now - @start_time
86
+ end
55
87
 
56
- def expire_proposed_facts
57
- cutoff = (Time.now - @config[:proposed_fact_ttl_days] * 86400).utc.iso8601
58
- @stats[:proposed_facts_expired] = @store.facts
59
- .where(status: "proposed")
60
- .where { created_at < cutoff }
61
- .update(status: "expired")
88
+ stats
62
89
  end
63
90
 
64
- def expire_disputed_facts
65
- cutoff = (Time.now - @config[:disputed_fact_ttl_days] * 86400).utc.iso8601
66
- @stats[:disputed_facts_expired] = @store.facts
67
- .where(status: "disputed")
68
- .where { created_at < cutoff }
69
- .update(status: "expired")
70
- end
71
-
72
- def prune_orphaned_provenance
73
- fact_ids = @store.facts.select(:id)
74
- @stats[:orphaned_provenance_deleted] = @store.provenance
75
- .exclude(fact_id: fact_ids)
76
- .delete
77
- end
91
+ private
78
92
 
79
- def prune_old_content
80
- cutoff = (Time.now - @config[:content_retention_days] * 86400).utc.iso8601
81
- referenced_ids = @store.provenance.exclude(content_item_id: nil).select(:content_item_id)
82
- prunable = @store.content_items
83
- .where { ingested_at < cutoff }
84
- .exclude(id: referenced_ids)
85
-
86
- # Remove FTS entries for content being pruned
87
- fts = ClaudeMemory::Index::LexicalFTS.new(@store)
88
- prunable.select(:id, :raw_text).each do |row|
89
- fts.remove_content_item(row[:id], row[:raw_text])
90
- rescue
91
- # FTS entry may not exist; skip
93
+ def build_maintenance(level)
94
+ config = case level
95
+ when :aggressive
96
+ {
97
+ proposed_fact_ttl_days: @config[:proposed_fact_ttl_days] / 2,
98
+ disputed_fact_ttl_days: @config[:disputed_fact_ttl_days] / 2,
99
+ content_retention_days: @config[:content_retention_days] / 2
100
+ }
101
+ else
102
+ @config.slice(:proposed_fact_ttl_days, :disputed_fact_ttl_days, :content_retention_days)
92
103
  end
93
104
 
94
- @stats[:old_content_pruned] = prunable.delete
105
+ Maintenance.new(@store, config: config)
95
106
  end
96
107
 
97
- def with_vec_index
98
- vec_index = @store.vector_index
99
- return unless vec_index.available?
100
- yield vec_index
101
- end
102
-
103
- def backfill_vec_index
104
- with_vec_index do |vec_index|
105
- @stats[:vec_backfilled] = vec_index.backfill_batch!(limit: 100)
106
- end
108
+ # Force-expire the oldest proposed or disputed facts regardless of TTL.
109
+ # Guarantees progress when normal and aggressive sweeps find nothing.
110
+ # Limited to 10 facts per invocation for safety.
111
+ def force_expire_oldest(limit: 10)
112
+ oldest_ids = @store.facts
113
+ .where(status: %w[proposed disputed])
114
+ .order(:created_at)
115
+ .limit(limit)
116
+ .select(:id)
117
+ .map { |r| r[:id] }
118
+
119
+ return 0 if oldest_ids.empty?
120
+
121
+ @store.facts
122
+ .where(id: oldest_ids)
123
+ .update(status: "expired")
107
124
  end
108
125
 
109
- def cleanup_vec_expired
110
- with_vec_index do |vec_index|
111
- # Remove vec0 entries for superseded/expired facts
112
- # (remove_embedding manages vec_indexed_at)
113
- stale_ids = @store.facts
114
- .where(status: %w[superseded expired])
115
- .where(Sequel.~(vec_indexed_at: nil))
116
- .select(:id)
117
- .limit(100)
118
- .map { |r| r[:id] }
119
-
120
- stale_ids.each { |fact_id| vec_index.remove_embedding(fact_id) }
121
- @stats[:vec_cleaned] = stale_ids.size
122
- end
126
+ def run_if_within_budget
127
+ return unless within_budget?
128
+ yield
123
129
  end
124
130
 
125
- def checkpoint_wal
126
- @store.checkpoint_wal
127
- @stats[:wal_checkpointed] = true
131
+ def within_budget?
132
+ budget = @config[:default_budget_seconds]
133
+ (Time.now - @start_time) < budget
128
134
  end
129
135
  end
130
136
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeMemory
4
- VERSION = "0.7.0"
4
+ VERSION = "0.7.1"
5
5
  end
data/lib/claude_memory.rb CHANGED
@@ -116,6 +116,7 @@ require_relative "claude_memory/resolve/predicate_policy"
116
116
  require_relative "claude_memory/resolve/resolver"
117
117
  require_relative "claude_memory/store/sqlite_store"
118
118
  require_relative "claude_memory/store/store_manager"
119
+ require_relative "claude_memory/sweep/maintenance"
119
120
  require_relative "claude_memory/sweep/sweeper"
120
121
  require_relative "claude_memory/version"
121
122
 
data/v0.6.0.ANNOUNCE ADDED
@@ -0,0 +1,32 @@
1
+ **ClaudeMemory v0.6.0 — Native Vector Search, Benchmarks, and Critical Bug Fixes**
2
+
3
+ New release of ClaudeMemory, the long-term self-managed memory gem for Claude Code.
4
+
5
+ **What's new:**
6
+
7
+ **sqlite-vec Native Vector Storage** — Vector search is now backed by sqlite-vec for fast KNN queries instead of loading all embeddings into Ruby. Embeddings are dual-written to both JSON and vec0 virtual tables, with automatic fallback if sqlite-vec isn't available. The `doctor` command reports vec coverage and the sweeper cleans up stale entries.
8
+
9
+ **SessionStart Context Injection** — Memory now automatically injects relevant facts at the start of each session via `hookSpecificOutput.additionalContext`, so Claude has context before you even ask.
10
+
11
+ **Database Maintenance** — New `compact` (VACUUM + integrity check) and `export` (JSON backup) commands for database housekeeping.
12
+
13
+ **Comparative Benchmarks (DevMemBench)** — Head-to-head retrieval benchmarks against QMD and grepai across 50 queries at 3 difficulty levels:
14
+
15
+ ```
16
+ Adapter Recall@5 MRR
17
+ QMD-Vector 0.842 0.930
18
+ ClaudeMemory (hybrid) 0.712 0.732
19
+ FTS-only 0.712 0.732
20
+ QMD-BM25 0.350 0.400
21
+ grepai 0.000 0.000
22
+ ```
23
+
24
+ QMD-Vector leads on easy/medium queries thanks to its custom fine-tuned query expansion model (Qwen3-1.7B). ClaudeMemory edges ahead on hard cross-category queries (0.358 vs 0.352 Recall@5) where multi-fact reasoning matters. Truth maintenance remains at 100% accuracy across all 100 test cases. Full benchmark suite runs locally at zero cost.
25
+
26
+ **Critical Bug Fixes:**
27
+ - **Recall returned no results** — `DualQueryTemplate` accessed stores before initializing them, silently returning empty arrays for all queries. This was the root cause of `claude-memory recall` never finding facts.
28
+ - **Doctor crashed with `no such module: vec0`** — Schema health checks tried to count rows in sqlite-vec virtual tables without loading the extension first.
29
+
30
+ 21 MCP tools · 22 CLI commands · 1,316 tests
31
+
32
+ <https://github.com/codenamev/claude_memory/releases/tag/v0.6.0>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: claude_memory
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Valentino Stoll
@@ -132,6 +132,7 @@ files:
132
132
  - docs/influence/episodic-memory.md
133
133
  - docs/influence/grepai.md
134
134
  - docs/influence/kbs.md
135
+ - docs/influence/lossless-claw.md
135
136
  - docs/influence/qmd.md
136
137
  - docs/organizational_memory_playbook.md
137
138
  - docs/plans/feature_adoption_plan.md
@@ -238,6 +239,7 @@ files:
238
239
  - lib/claude_memory/ingest/tool_filter.rb
239
240
  - lib/claude_memory/ingest/transcript_reader.rb
240
241
  - lib/claude_memory/logging/logger.rb
242
+ - lib/claude_memory/mcp/error_classifier.rb
241
243
  - lib/claude_memory/mcp/instructions_builder.rb
242
244
  - lib/claude_memory/mcp/query_guide.rb
243
245
  - lib/claude_memory/mcp/response_formatter.rb
@@ -256,6 +258,7 @@ files:
256
258
  - lib/claude_memory/shortcuts.rb
257
259
  - lib/claude_memory/store/sqlite_store.rb
258
260
  - lib/claude_memory/store/store_manager.rb
261
+ - lib/claude_memory/sweep/maintenance.rb
259
262
  - lib/claude_memory/sweep/sweeper.rb
260
263
  - lib/claude_memory/templates/hooks.example.json
261
264
  - lib/claude_memory/templates/output-styles/memory-aware.md
@@ -269,6 +272,7 @@ files:
269
272
  - skills/memory-first-workflow/SKILL.md
270
273
  - skills/memory/SKILL.md
271
274
  - skills/setup-memory/SKILL.md
275
+ - v0.6.0.ANNOUNCE
272
276
  homepage: https://github.com/codenamev/claude_memory
273
277
  licenses:
274
278
  - MIT