Fact lifecycle decay: two-stage expiry with explicit ratification (#14) - #16
Conversation
…enamev#14 - Migration 021: reaffirmed_at + expiring_since columns on facts - Maintenance#mark_expiring_facts: stale (created + last recall + reaffirmed all past stale_fact_threshold_days, default 180) active facts → expiring - Maintenance#expire_unratified_facts: expiring past ratify_window_days (default 30) without ratification → expired (never deleted) - Wired into Sweeper budget loop; specs for both stages Co-Authored-By: Claude Fable 5 <[email protected]>
…enamev#14) - mark_expiring_facts no longer counts last_recalled_at as freshness: RecallTimestampRefresher bulk-touches that column on every passive recall, which self-defeated decay. Clock = created_at + reaffirmed_at. - StalenessAnnotator: expiring lifecycle marker (any predicate) that outranks the heuristic stale marker — the ratification prompt. - build_facts_dataset selects reaffirmed_at + expiring_since; shortcuts fetch includes expiring facts alongside active. Co-Authored-By: Claude Fable 5 <[email protected]>
…namev#14) Ratification is the only signal that resets the decay clock — passive recall never does. ratify_fact also restores expired facts; superseded/ rejected/disputed are refused.
Co-Authored-By: Claude Fable 5 <[email protected]>
| add_column :reaffirmed_at, String # ISO 8601, explicit ratification only | ||
| add_column :expiring_since, String # ISO 8601, entered expiring stage |
There was a problem hiding this comment.
Deliberate, following the schema-wide convention: every timestamp in this DB is an ISO-8601 UTC string — created_at (001), vec_indexed_at (012), last_recalled_at (017), promoted_at (020). SQLite has no native datetime type, and all the existing query logic compares these lexicographically (ISO-8601 UTC sorts chronologically), e.g. the sweeper's expiring_since < cutoff. Typing just these two as DateTime would make them the only columns Sequel round-trips as Time objects and break symmetry with every comparison in the codebase.
If you'd rather move the whole schema to typed timestamps, I'd do that as its own migration + issue rather than smuggling two odd columns in here. Happy to file it — say the word.
There was a problem hiding this comment.
File it for investigation for performance. Not required if working well as-is
Implements the measured stigmergy-decay scope from #14 (Val-directed 2026-08-31).
What
reaffirmed_at+expiring_sinceon facts; two-stage lifecycleactive → expiring → expired.mark_expiring_factskeys offcreated_at/reaffirmed_atonly —RecallTimestampRefresher's bulk touch oflast_recalled_atno longer resets the clock (the self-defeat noted in Fact lifecycle: expiry-unless-reaffirmed + ratification surface #14).StalenessAnnotatoradds an expiring marker on all predicates (outranks the heuristic stale marker — it is the ratification prompt); shortcuts recall includesexpiringalongsideactive.store.ratify_fact(restores expired too; refuses rejected/superseded/disputed), MCP toolsmemory.list_expiring_facts+memory.ratify_fact, CLIratifycommand.Out of scope (still gated on your read of the issues)
Provenance/authority (#13) and relation edges (#15) — implementation beans exist, gated.
Testing
Targeted specs green (maintenance, sweeper, dashboard, store); full suite via CI.
🤖 Generated with Claude Code