Handler: retention.ttl_prune
Generic TTL-based row deleter. Drops rows from row.table_name where row.age_column is older than row.ttl_days days. Optional row.filter_sql narrows the scope so the same table can host multiple policies (e.g. embeddings partitioned by source_table).
Row configuration
Safety
- Batched deletes prevent long exclusive locks on large tables. The handler loops with
ctid-keyedDELETEstatements until a batch returns fewer rows thanbatch_size. - Identifier validation rejects anything non-alphanumeric in
table_name/age_columnat runtime. String interpolation is used to build SQL (asyncpg can’t parameterize identifiers), but a malformed seed migration can’t slip SQL injection through. - Dry run gives a count-only preview before enabling.
Operator runbook
Enabling a seeded policy
Creating a new policy
Seeded policies (all disabled)
| Name | Table | Filter | TTL |
|---|---|---|---|
embeddings.claude_sessions | embeddings | source_table = 'claude_sessions' | 30 days |
embeddings.audit | embeddings | source_table = 'audit' | 90 days |
embeddings.brain | embeddings | source_table = 'brain' | 180 days |
audit_log | audit_log | — | 90 days |
brain_decisions | brain_decisions | — | 90 days |
Disabling
Expected outcome
Running the seededembeddings.claude_sessions policy once against the current DB (13,105 total embeddings, 7,095 from claude_sessions, most from the Apr 20 backfill) will delete roughly 6,369 rows — the Apr 20 backfill dump — and leave the ~200/day steady-state untouched.
Related
- Framework overview: Integrations
- Database schema reference:
docs/architecture/database-schema.md - Sibling handlers:
retention.downsample,retention.temporal_summarize(deferred) - GH-110 (retention framework issue)
- GH-106 (original embeddings retention issue)