Commit 6d2cecc
feat(memory): episode lifecycle — dedup-on-write + eviction (cap/TTL) (#17)
* feat(memory): episode lifecycle — dedup-on-write + eviction (cap/TTL)
EpisodeStore had no dedup and no eviction: episodes accumulated forever
and near-duplicate sessions piled up unbounded (disk growth, noisier
recall). This adds bounded, de-duplicated episode storage, config-gated
with safe defaults, no on-disk format change.
- Dedup-on-write: a new episode whose cosine similarity to an existing one
is >= episode_dedup_threshold (default 0.92) REPLACES it (newest-wins).
Uses an ephemeral RP embedder (the NewRPRanker primitive) over full
on-disk summaries — never the shared dirty-rebuild vector index, avoiding
mid-write re-entrancy. Provenance-gated: an untrusted near-dup can never
evict a trusted/approved episode (trustRank mirrors the recall filter).
- Eviction: prune-on-write applies TTL (episode_ttl_days, default 0/off)
then a count cap (max_episodes, default 500), deleting both the .md file
and the index entry. Crash-safe order: files -> writeIndex -> markDirty.
Also exposes EpisodeStore.Prune() for session-end/CLI use.
- Locking: dedup + .md write + index update + prune + markDirty now happen
under a single e.mu hold (new writeLocked). Fixes a latent bug where
re-writing the same sessionID appended a duplicate index entry.
- Config: EpisodeDedupThreshold / MaxEpisodes / EpisodeTTLDays wired through
MemoryConfig, DefaultMemoryConfig, both overlay sites, and a new
NewEpisodeStoreWithLifecycle (bare NewEpisodeStore keeps lifecycle off, so
existing callers/tests are unaffected). Documented in docs/CONFIG.md.
Fact supersession is deferred (facts lack per-entry metadata; already
covered by merge-on-write + session-end LLM consolidation).
Tests: dedup replace/threshold/disabled, provenance safety (both
directions), eviction by count + TTL, TTL-disabled, self-overwrite
regression, evicted-id absent from recall, -race concurrency (16
goroutines), config defaults + overlay-to-store wiring.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(memory): validate sessionID before deleting episode files
Adversarial review (AI Verification Protocol) found a path-traversal
defense-in-depth gap: eviction/dedup called os.Remove on sessionIDs read
straight from index.json without validation, so a crafted/corrupted index
entry (e.g. "../victim") could delete a .md file OUTSIDE the episodes dir.
Every other file op in the package (Read/Write/Promote) already validates.
Add removeEpisodeFile(sessionID) which calls session.ValidateSessionID
before os.Remove, and route all three eviction/dedup deletions through it.
Adds a traversal-safe regression test.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>1 parent 7662fe0 commit 6d2cecc
5 files changed
Lines changed: 608 additions & 21 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
203 | 203 | | |
204 | 204 | | |
205 | 205 | | |
206 | | - | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
207 | 210 | | |
208 | 211 | | |
209 | 212 | | |
| |||
225 | 228 | | |
226 | 229 | | |
227 | 230 | | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
228 | 234 | | |
229 | 235 | | |
230 | 236 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
859 | 859 | | |
860 | 860 | | |
861 | 861 | | |
| 862 | + | |
| 863 | + | |
| 864 | + | |
| 865 | + | |
| 866 | + | |
| 867 | + | |
| 868 | + | |
| 869 | + | |
| 870 | + | |
862 | 871 | | |
863 | 872 | | |
864 | 873 | | |
| |||
0 commit comments