Every memory has a lifetime — minutes, days, or forever. ai-memory exposes eight independent TTL dials from per-write override to daemon-wide config to namespace policy to GC cadence. Default behavior covers most use cases. When it doesn't, every dial is reachable from one of three layers: the memory itself, the daemon config, or the namespace standard.
Higher dials win. A per-write expires_at overrides everything. Tier defaults fill in the gaps. Daemon config customizes the tier defaults globally. Namespace standards (v0.7 spec §3) will let policy enforce caps; today the gate is governance-on-promote.
validate_expires_at); lenient on updates (validate_expires_at_format).expires_at = now() + ttl_secs. Validated: must be positive, ≤ 31 536 000 (1 year — caps "accidental immortality" from typos). When both expires_at and ttl_secs are set, expires_at wins.expires_at nor ttl_secs is set, the tier's default applies: Short = 6h, Mid = 7d, Long = no TTL. The default tier is Mid if you don't pick one.config.toml under [ttl]. Applies to every Short-tier memory created by this daemon. 0 disables expiry for the tier. Useful when the operator wants tighter or looser short-term retention than the 6h default.short_ttl_secs but for Mid tier. Common override: extend Mid to 30 days (2592000) for longer project cycles.None). Setting this turns Long into a finite tier — useful for compliance scenarios where "forever" is too long. 0 = no TTL (matches default).memory_get, recall hit, search hit), its expires_at bumps forward by the tier's extend_secs. Memories that get used keep earning more time. Defaults: Short +1h, Mid +1d. Long has no extend (no TTL to extend).true (default): expired memories move to archived_memories — restorable. false: hard delete on expiry — gone. The archive table itself can be auto-purged via auto_purge_archive(max_days) for a soft-then-hard retention pattern.Garbage collection is on-demand and observable. There's no magic background daemon — the operator decides when GC runs, either via the MCP memory_gc tool, the HTTP /api/v1/gc endpoint, or the curator cycle (src/curator.rs). Each invocation logs how many rows it touched.
When you don't want to wait for TTL — sweep on demand. memory_forget takes a namespace + FTS pattern + optional tier filter, archives every match, returns the count. Same effect as letting them expire, but you decide when.
Forget vs. delete vs. forget-pattern:
archive_on_gc).archive_on_gc. Operator-invoked or curator-cycle-driven.