Module: ClaudeMemory::Recall::StalenessAnnotator

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

Overview

Pure function. Given a fact hash, returns a human-readable staleness marker for single-value facts that are old and unconfirmed, or nil.

Single-value predicates (uses_database / deployment_platform / auth_method) are exclusive claims — “the project uses X.” Claude follows them authoritatively, so a stale single-value fact is the most dangerous kind of memory: the 0.12 harm benchmark caught Claude emitting ‘git push heroku` from a stale deployment_platform fact with no hedge (docs/1_0_punchlist.md #3 / #15). This annotator surfaces the uncertainty inline at context-injection time so Claude can hedge or verify instead of blindly following.

Multi-value predicates (convention, decision, uses_framework, …) are NOT annotated: they accumulate, so one stale entry doesn’t carry the same authoritative weight, and flagging them would just add noise.

A fact is stale-for-injection when BOTH hold:

- the claim is old: valid_from (or created_at fallback) is older
  than threshold_days — a freshly recorded fact is never stale even
  if it describes something historical, and
- it hasn't been confirmed recently: last_recalled_at is null or
  older than threshold_days — a fact that's been recalled lately is
  implicitly re-validated by use.

No side effects; safe to call per-fact in the context-injection loop.

Constant Summary collapse

DEFAULT_THRESHOLD_DAYS =
180

Class Method Summary collapse

Class Method Details

.marker_for(fact, now: Time.now.utc, threshold_days: DEFAULT_THRESHOLD_DAYS) ⇒ String?

Returns marker text, or nil when not stale / not guarded.

Parameters:

  • fact (Hash)

    needs :predicate; reads :valid_from, :created_at, :last_recalled_at when present

  • now (Time) (defaults to: Time.now.utc)
  • threshold_days (Integer) (defaults to: DEFAULT_THRESHOLD_DAYS)

Returns:

  • (String, nil)

    marker text, or nil when not stale / not guarded



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# File 'lib/claude_memory/recall/staleness_annotator.rb', line 42

def marker_for(fact, now: Time.now.utc, threshold_days: DEFAULT_THRESHOLD_DAYS)
  return nil unless Resolve::PredicatePolicy.single?(fact[:predicate].to_s)

  established = parse_time(fact[:valid_from]) || parse_time(fact[:created_at])
  return nil unless established

  cutoff = now - threshold_days * 86_400
  return nil unless established < cutoff

  last_seen = parse_time(fact[:last_recalled_at])
  return nil if last_seen && last_seen >= cutoff

  months = ((now - established) / (30 * 86_400)).round
  "⚠ stale: recorded #{established.strftime("%Y-%m-%d")}, " \
    "not confirmed in ~#{months}mo — verify before relying"
end

.parse_time(value) ⇒ Object



64
65
66
67
68
69
70
# File 'lib/claude_memory/recall/staleness_annotator.rb', line 64

def parse_time(value)
  return nil if value.nil?
  return value.utc if value.is_a?(Time)
  Time.parse(value.to_s).utc
rescue ArgumentError
  nil
end

.stale?(fact, now: Time.now.utc, threshold_days: DEFAULT_THRESHOLD_DAYS) ⇒ Boolean

Returns true when marker_for would return a marker.

Returns:

  • (Boolean)

    true when marker_for would return a marker



60
61
62
# File 'lib/claude_memory/recall/staleness_annotator.rb', line 60

def stale?(fact, now: Time.now.utc, threshold_days: DEFAULT_THRESHOLD_DAYS)
  !marker_for(fact, now: now, threshold_days: threshold_days).nil?
end