Skip to content

Fact lifecycle: expiry-unless-reaffirmed + ratification surface #14

Description

@minerva-sky

Design proposal (companion to #13). Grounded in current code; no implementation yet.

What exists

  • Recall::StaleDetector (#35) surfaces access-stale active facts (last_recalled_at past threshold), explicitly read-only: "the sweeper never acts on this."
  • Sweep::RecallTimestampRefresher touches last_recalled_at on recall/context-injection.
  • Fact statuses: active / superseded / rejected (plus disputed per Provenance + authority on facts; keep and surface contested claims #13). Observations already have an expired retire path (observation_writes.rb:79); facts have no lifecycle terminal for "nobody has needed or confirmed this in a long time."

Gap

Staleness is a report, not a lifecycle. A stale-but-wrong fact stays active and keeps winning recall forever unless the user happens to read the stale list. And the only freshness signal is passive recall — which self-defeats: recalling a fact touches last_recalled_at, so frequently-recalled-but-wrong facts can never age out.

Proposal

Two-stage expiry with distinct signals per stage:

  1. activeexpiring (sweeper, automatic): stale past threshold_days per existing StaleDetector predicate. expiring facts still recall, but annotated (staleness_annotator already exists) and down-weighted. Passive recall during this window does NOT reset the clock back to active — that's the self-defeat above.
  2. expiringexpired (sweeper, automatic): after a second window (ratify_window_days) with no ratification. expired facts are excluded from default recall (like superseded), never deleted, and restorable.
  3. Ratification (explicit, user/agent-initiated): new reaffirmed_at column, set only by the ratification surface — never by passive recall. Ratifying returns the fact to active and resets both clocks.

Ratification surface

  • CLI: claude-memory ratify — interactive bulk pass over expiring facts (keep / expire / edit), modeled on the existing stale-list review flow.
  • MCP: list_expiring_facts + ratify_fact(id) so agents can raise expiring facts mid-session ("still true that X?") — the cheapest honest signal, since the user is present.
  • Dashboard: expiring count next to the existing stale count in the sidebar.

Open questions

  • Should expiring facts lose conflict-resolution priority in Resolve::Resolver? (Lean: yes — a contested claim between active and expiring should prefer active.)
  • Per-predicate thresholds? Identity facts (name, timezone) age slower than state facts (current project). PredicatePolicy is the natural home.
  • Does ratification need authority per Provenance + authority on facts; keep and surface contested claims #13? (Lean: yes — user ratification > agent ratification, recorded in the same provenance columns.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions