Class: ClaudeMemory::Sweep::Maintenance
- Inherits:
-
Object
- Object
- ClaudeMemory::Sweep::Maintenance
- Defined in:
- lib/claude_memory/sweep/maintenance.rb
Overview
Clean separation of individual maintenance operations from Sweeper’s budget-management orchestration. Each method performs a single operation and returns the count of affected records.
Source: QMD v2.0.1 Maintenance class pattern
Constant Summary collapse
- RESTORE_STOPWORDS =
Short / noise tokens dropped before Jaccard comparison. Intentionally minimal — we want conservative token extraction that still treats “Rails 8.0” and “Rails 8.1” as overlapping.
%w[for the and with via of in on to by is are].to_set.freeze
- RESTORE_JACCARD_THRESHOLD =
0.5- DEFAULT_CONFIG =
{ proposed_fact_ttl_days: 14, disputed_fact_ttl_days: 30, content_retention_days: 30, mcp_tool_call_retention_days: 90 }.freeze
Instance Attribute Summary collapse
-
#store ⇒ Object
readonly
Returns the value of attribute store.
Instance Method Summary collapse
-
#backfill_vec_index(limit: 100) ⇒ Object
Backfill vector index for unindexed facts.
-
#checkpoint_wal ⇒ Object
Checkpoint the SQLite WAL file for compaction.
-
#cleanup_vec_expired(limit: 100) ⇒ Object
Remove vector embeddings for superseded/expired facts.
-
#expire_disputed_facts ⇒ Object
Expire disputed facts older than TTL.
-
#expire_proposed_facts ⇒ Object
Expire proposed facts older than TTL.
-
#initialize(store, config: {}) ⇒ Maintenance
constructor
A new instance of Maintenance.
-
#prune_old_content ⇒ Object
Delete old content items not referenced by any provenance.
-
#prune_old_mcp_tool_calls ⇒ Object
Delete MCP tool-call telemetry rows older than retention window.
-
#prune_orphaned_provenance ⇒ Object
Delete provenance records referencing non-existent facts.
-
#restore_multi_value_supersessions(predicate:, dry_run: false) ⇒ Hash
Restore superseded facts in a (subject, predicate) slot that were only superseded because of an obsolete single-value classification.
-
#vacuum ⇒ Object
Run SQLite VACUUM to reclaim space.
Constructor Details
#initialize(store, config: {}) ⇒ Maintenance
Returns a new instance of Maintenance.
25 26 27 28 |
# File 'lib/claude_memory/sweep/maintenance.rb', line 25 def initialize(store, config: {}) @store = store @config = DEFAULT_CONFIG.merge(config) end |
Instance Attribute Details
#store ⇒ Object (readonly)
Returns the value of attribute store.
23 24 25 |
# File 'lib/claude_memory/sweep/maintenance.rb', line 23 def store @store end |
Instance Method Details
#backfill_vec_index(limit: 100) ⇒ Object
Backfill vector index for unindexed facts. Returns: Integer count of backfilled embeddings (0 if unavailable)
81 82 83 84 85 86 |
# File 'lib/claude_memory/sweep/maintenance.rb', line 81 def backfill_vec_index(limit: 100) with_vec_index do |vec_index| return vec_index.backfill_batch!(limit: limit) end 0 end |
#checkpoint_wal ⇒ Object
Checkpoint the SQLite WAL file for compaction. Returns: true
186 187 188 189 |
# File 'lib/claude_memory/sweep/maintenance.rb', line 186 def checkpoint_wal @store.checkpoint_wal true end |
#cleanup_vec_expired(limit: 100) ⇒ Object
Remove vector embeddings for superseded/expired facts. Returns: Integer count of cleaned embeddings (0 if unavailable)
90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
# File 'lib/claude_memory/sweep/maintenance.rb', line 90 def cleanup_vec_expired(limit: 100) with_vec_index do |vec_index| stale_ids = @store.facts .where(status: %w[superseded expired]) .where(Sequel.~(vec_indexed_at: nil)) .select(:id) .limit(limit) .map { |r| r[:id] } stale_ids.each { |fact_id| vec_index.(fact_id) } return stale_ids.size end 0 end |
#expire_disputed_facts ⇒ Object
Expire disputed facts older than TTL. Returns: Integer count of expired facts
42 43 44 45 46 47 48 |
# File 'lib/claude_memory/sweep/maintenance.rb', line 42 def expire_disputed_facts cutoff = cutoff_time(@config[:disputed_fact_ttl_days]) @store.facts .where(status: "disputed") .where { created_at < cutoff } .update(status: "expired") end |
#expire_proposed_facts ⇒ Object
Expire proposed facts older than TTL. Returns: Integer count of expired facts
32 33 34 35 36 37 38 |
# File 'lib/claude_memory/sweep/maintenance.rb', line 32 def expire_proposed_facts cutoff = cutoff_time(@config[:proposed_fact_ttl_days]) @store.facts .where(status: "proposed") .where { created_at < cutoff } .update(status: "expired") end |
#prune_old_content ⇒ Object
Delete old content items not referenced by any provenance. Also removes their FTS index entries. Returns: Integer count of deleted content items
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
# File 'lib/claude_memory/sweep/maintenance.rb', line 62 def prune_old_content cutoff = cutoff_time(@config[:content_retention_days]) referenced_ids = @store.provenance.exclude(content_item_id: nil).select(:content_item_id) prunable = @store.content_items .where { ingested_at < cutoff } .exclude(id: referenced_ids) fts = ClaudeMemory::Index::LexicalFTS.new(@store) prunable.select(:id, :raw_text).each do |row| fts.remove_content_item(row[:id], row[:raw_text]) rescue # FTS entry may not exist; skip end prunable.delete end |
#prune_old_mcp_tool_calls ⇒ Object
Delete MCP tool-call telemetry rows older than retention window. Returns: Integer count of deleted rows (0 if table missing).
177 178 179 180 181 182 |
# File 'lib/claude_memory/sweep/maintenance.rb', line 177 def prune_old_mcp_tool_calls return 0 unless @store.db.table_exists?(:mcp_tool_calls) cutoff = cutoff_time(@config[:mcp_tool_call_retention_days]) @store.mcp_tool_calls.where { called_at < cutoff }.delete end |
#prune_orphaned_provenance ⇒ Object
Delete provenance records referencing non-existent facts. Returns: Integer count of deleted provenance rows
52 53 54 55 56 57 |
# File 'lib/claude_memory/sweep/maintenance.rb', line 52 def prune_orphaned_provenance fact_ids = @store.facts.select(:id) @store.provenance .exclude(fact_id: fact_ids) .delete end |
#restore_multi_value_supersessions(predicate:, dry_run: false) ⇒ Hash
Restore superseded facts in a (subject, predicate) slot that were only superseded because of an obsolete single-value classification. Uses Jaccard-based token overlap to distinguish bug-superseded facts (token-disjoint siblings) from legitimate corrections (overlapping siblings).
Refuses to run on predicates still classified as single-value — they should stay superseded by design.
Never touches status: “rejected” facts (explicit user decisions).
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/claude_memory/sweep/maintenance.rb', line 117 def restore_multi_value_supersessions(predicate:, dry_run: false) if ClaudeMemory::Resolve::PredicatePolicy.single?(predicate) raise ArgumentError, "Predicate '#{predicate}' is still classified single-value; refusing to restore" end result = {inspected: 0, restored: 0, skipped_ambiguous: 0, skipped_rejected: 0, decisions: []} rows_by_subject = @store.facts .where(predicate: predicate) .exclude(status: "rejected") .select(:id, :subject_entity_id, :object_literal, :status) .all .group_by { |r| r[:subject_entity_id] } rejected_by_subject = @store.facts .where(predicate: predicate, status: "rejected") .select(:id, :subject_entity_id, :object_literal) .all .group_by { |r| r[:subject_entity_id] } @store.db.transaction do rows_by_subject.each do |subject_id, rows| rejected_rows = rejected_by_subject[subject_id] || [] siblings = rows + rejected_rows rows.each do |candidate| next unless candidate[:status] == "superseded" result[:inspected] += 1 candidate_tokens = restore_tokenize(candidate[:object_literal]) ambiguous_against = find_overlapping_siblings(candidate, siblings, candidate_tokens) if ambiguous_against.empty? result[:restored] += 1 result[:decisions] << { subject_entity_id: subject_id, fact_id: candidate[:id], object: candidate[:object_literal], action: :restore } restore_fact!(candidate[:id]) unless dry_run else result[:skipped_ambiguous] += 1 result[:decisions] << { subject_entity_id: subject_id, fact_id: candidate[:id], object: candidate[:object_literal], action: :skip_ambiguous, overlaps_with: ambiguous_against.map { |s| s[:object_literal] } } end end end end result end |
#vacuum ⇒ Object
Run SQLite VACUUM to reclaim space. Returns: true
193 194 195 196 |
# File 'lib/claude_memory/sweep/maintenance.rb', line 193 def vacuum @store.db.run("VACUUM") true end |