Module: ClaudeMemory::Recall::StaleDetector

Defined in:
lib/claude_memory/recall/stale_detector.rb

Overview

#35 access-based staleness — read-only query layer over the last_recalled_at column populated by Sweep::RecallTimestampRefresher.

An active fact is “stale” when:

  • It hasn’t been recalled or context-injected within ‘threshold_days` (last_recalled_at < cutoff OR last_recalled_at is NULL), AND

  • It was created before the cutoff too — freshly extracted facts aren’t dead weight, they just haven’t had a chance to be used.

No auto-deletion. The point is to surface a count and a list to the user so they can review and reject; the sweeper never acts on this.

Class Method Summary collapse

Class Method Details

.stale_count(manager, threshold_days:) ⇒ Integer

Scope-agnostic count helper for the dashboard sidebar. Avoids materializing rows when only a count is needed.

Returns:

  • (Integer)

    total stale facts across both stores



42
43
44
45
46
47
48
49
50
51
# File 'lib/claude_memory/recall/stale_detector.rb', line 42

def stale_count(manager, threshold_days:)
  cutoff = (Time.now.utc - threshold_days * 86_400).iso8601
  count = 0
  %w[project global].each do |scope|
    store = manager.store_if_exists(scope)
    next unless store
    count += stale_dataset(store, cutoff).count
  end
  count
end

.stale_dataset(store, cutoff) ⇒ Object



53
54
55
56
57
58
# File 'lib/claude_memory/recall/stale_detector.rb', line 53

def stale_dataset(store, cutoff)
  store.facts
    .where(status: "active")
    .where { created_at < cutoff }
    .where { (last_recalled_at < cutoff) | {last_recalled_at: nil} }
end

.stale_facts(manager, threshold_days:, limit: 50) ⇒ Hash

Returns […], global: […], total: Int.

Parameters:

  • manager (Store::StoreManager)
  • threshold_days (Integer)

    grace window in days

  • limit (Integer) (defaults to: 50)

    max rows per scope (0 = unlimited)

Returns:

  • (Hash)

    […], global: […], total: Int



23
24
25
26
27
28
29
30
31
32
33
34
35
36
# File 'lib/claude_memory/recall/stale_detector.rb', line 23

def stale_facts(manager, threshold_days:, limit: 50)
  cutoff = (Time.now.utc - threshold_days * 86_400).iso8601
  result = {project: [], global: [], total: 0}

  %w[project global].each do |scope|
    store = manager.store_if_exists(scope)
    next unless store
    rows = stale_rows_for(store, cutoff, limit)
    result[scope.to_sym] = rows
    result[:total] += rows.size
  end

  result
end

.stale_rows_for(store, cutoff, limit) ⇒ Object



60
61
62
63
64
# File 'lib/claude_memory/recall/stale_detector.rb', line 60

def stale_rows_for(store, cutoff, limit)
  ds = stale_dataset(store, cutoff).order(Sequel.asc(:last_recalled_at)).order_append(:created_at)
  ds = ds.limit(limit) if limit > 0
  ds.all
end