This document is the implementation and release gate for issue #44:
ForgettingService scan, score, fade/prune, duplicate handling, and media
cleanup.
The rule for this branch is simple: write the expected behavior down first, implement against it, then verify the branch against the same checklist before opening the PR.
Included:
forgetting/service.pyForgettingReportand per-record decision entries- config knobs for prune thresholds, fade thresholds, fade behavior, and procedural low-performance overrides
- semantic duplicate-cluster resolution for the forgetting cycle
- confirmed supersession forced-prune handling
- fade via store
replace() - prune via store
delete()followed by owned-media deletion - per-record forgetting events and cycle summary events
- automated tests for planning, execution, idempotency, and event payloads
Explicitly not included:
- LLM judging for duplicate or contradiction resolution
- transactional or cross-store atomic writes
- Chroma compaction/rebuild automation
- API/CLI endpoints for manually triggering the forgetting cycle
- benchmark claims beyond a local sanity check on duplicate-cluster handling
The cycle operates in two phases:
- Plan from a single snapshot:
scan stores, detect duplicate clusters, resolve forced actions, compute decay
scores with one shared
cycle_now, and build the full decisions list. - Execute from the completed plan: apply fades, then prune records, then delete owned media for successfully pruned records, then emit the cycle summary event.
dry_run=True uses the same planning path but skips the mutation phase.
- Semantic likely-duplicate pairs are collapsed into connected components.
- Each component produces one survivor.
- The survivor is chosen by the deterministic chain:
supersession state, importance, access count, recency (
created_at), ID. - All non-survivors are forced-pruned with reason
likely_duplicate. - The duplicate resolver must be swappable later, but this PR ships the deterministic default.
When multiple reasons could apply, the winning reason order is:
supersededlikely_duplicatelow_performancetime_decay
Confirmed supersession only forces prune when the successor record still exists.
Per memory type:
- score
< prune_threshold->prune - prune_threshold
<= score < fade_threshold->fade - score
>= fade_threshold->keep
Fade mutates importance with:
new_importance = max(FADE_FLOOR, old_importance * FADE_FACTOR)
0.0 remains reserved for confirmed supersession.
- If
wilson_score < PROCEDURAL_LOW_PERF_WILSON_THRESHOLDandtotal_outcomes >= PROCEDURAL_LOW_PERF_MIN_OUTCOMES, forced-prune with reasonlow_performance. - Otherwise low performance can still force
fade, but does not forceprune.
ForgettingReport must include:
- aggregate counters
by_typeduplicates_flaggeddecisions
Each decision entry must include:
record_idmemory_typeactionreason(nullfor retained records with no forgetting pressure)scoremedia_deleted
Events:
- real run: per-record
memory.faded, per-recordmemory.pruned, summaryforgetting.cycle_completed - dry run: summary
forgetting.cycle_dry_runonly
run_cycle(dry_run=True)returns a report with derived aggregate counters that exactly match thedecisionslist.dry_run=Truedoes not mutate any record, delete any record, or delete any media file.- All decisions in one cycle use the same
cycle_nowreference time. - The report includes keep, fade, and prune decisions with stable reasons, and
keep decisions above the fade threshold carry
reason = null. - Top-level counts are derived from decisions rather than maintained separately.
- A duplicate pair produces one survivor and one forced prune.
- A transitive duplicate cluster (
A~B,B~C) is resolved component-wise with exactly one survivor. - The survivor is chosen by importance before access count, access count before recency, recency before ID.
- Duplicate resolution is deterministic regardless of pair iteration order.
- Non-winning cluster members are reported as
likely_duplicateprunes.
- A semantic record with
superseded_byand an existing successor is forced to prune even if its decay score would keep or fade it. - If the successor record is missing, supersession does not force prune and the record falls back to ordinary cycle logic.
- Supersession reason outranks all other reasons in the report and emitted event payloads.
- Semantic records respect semantic prune and fade thresholds.
- Episodic records respect episodic prune and fade thresholds.
- Procedural records respect procedural prune and fade thresholds when no low-performance override applies.
- Faded records persist with reduced importance using
FADE_FACTORandFADE_FLOOR. - Fading never sets importance to
0.0unless the record was already superseded outside the fade path.
- Low Wilson score with insufficient outcomes does not force prune.
- Low Wilson score with enough outcomes forces prune.
- Low performance without prune-level evidence forces fade.
low_performanceoutranks ordinarytime_decaywhen both apply.
- Real runs apply fades through store
replace()only after planning completes. - Real runs delete records through store
delete()only after fade execution is complete. - Owned media is deleted only after the owning record is successfully deleted.
- Missing records during execution are treated as skipped/idempotent cases, not counted as successful prunes.
- Missing media files do not increment
media_deleted. - Batched prune execution processes records in groups of 10.
memory.fadedis emitted once per actual fade and includes record id, memory type, reason, old importance, and new importance.memory.prunedis emitted once per actual prune and includes record id, memory type, reason, and media context.forgetting.cycle_completedis emitted once per real run with the full report payload.forgetting.cycle_dry_runis emitted once per dry run with the projected report payload.- Dry runs emit no per-record mutation events.
- Pruned records return
Nonefromget_by_id. - Faded records remain retrievable and show the updated importance.
- Existing contradiction and decay tests still pass unchanged.
- Existing store and event integration tests still pass unchanged.
These checks happen after automated tests pass.
- Seed one semantic duplicate cluster and one superseded semantic record, run the cycle manually in Python, and inspect the returned report for one survivor per cluster and one immediate supersession prune.
- Seed one media-backed episodic record that lands in prune and confirm the record disappears before the owned media file is removed.
- Seed one procedural record that lands in fade and confirm its importance is
halved but remains above
FADE_FLOORwhen applicable. - Run a dry cycle against the same dataset and confirm the report matches the real-run plan while the database and media files remain unchanged.
No new API surface is planned in this PR, so there are no required curl
acceptance checks for merge. If this branch grows an endpoint or admin trigger,
the API contract and curl cases must be added here before shipping.
- Verify bucket boundaries exactly at prune and fade thresholds.
- Verify fade math:
new_importance = max(FADE_FLOOR, old_importance * FADE_FACTOR). - Verify procedural prune override boundary at:
wilson_score < PROCEDURAL_LOW_PERF_WILSON_THRESHOLDandtotal_outcomes >= PROCEDURAL_LOW_PERF_MIN_OUTCOMES.
This PR does not claim production-scale optimization, but it should include one local sanity check:
- Duplicate-cluster planning on a moderate synthetic semantic dataset should complete without pathological order sensitivity or exploding decision counts.
If benchmark code is added, it should be documented in the PR description but kept out of the merge gate unless it is deterministic offline.
The PR can be opened only when all of the following are true:
testing.mdstill matches the shipped implementation.- New unit and integration tests for the forgetting cycle pass locally.
- Existing relevant test suites still pass locally, or any unrelated pre-existing failures are explicitly called out.
- Manual verification for dry run, fade, prune, duplicate resolution, and media cleanup is completed.
- The PR description explains the locked behavior: component-wise duplicate resolution, staged execution, explicit fade bands, and swappable duplicate resolver design.