How `memory_write_page` with `expires_at` Interacts with the Forget Sweep in ai-memory
memory_write_page stores an optional expires_at timestamp in the page's front-matter; once this timestamp passes, the page becomes invisible to queries, and the daily memory_forget_sweep job permanently deletes both the markdown file and its version ancestry from the SQLite store.
The ai-memory system provides time-bounded memory pages through a simple but rigorous TTL mechanism. When you write a page with an expiration date, the system follows a three-stage lifecycle: active visibility, query filtering, and eventual hard deletion. This article explains exactly how these stages work according to the source code in akitaonrails/ai-memory.
Writing a Page with expires_at
The memory_write_page function accepts an optional expires_at field in RFC 3339 format. According to the wiki crate implementation in crates/ai-memory-wiki/src/wiki.rs, the Wiki::write_page method extracts this value from the page's front-matter and forwards it to the persistent store.
The value is stored as Option<jiff::Timestamp> in the pages table's expires_at column. This design keeps TTL metadata alongside the content without requiring separate indexing tables.
# Write a page that expires at end of 2026
mcp memory_write_page '{
"path":"notes/project-plan.md",
"title":"Project Plan",
"body":"Milestones …",
"expires_at":"2026-12-31T23:59:59Z"
}'
The expires_at field accepts any valid RFC 3339 timestamp. Omitting it creates a page with no expiration.
Query Filtering: How Expired Pages Disappear
Once a page's expires_at timestamp passes, it immediately becomes invisible to normal queries. This happens through a defensive SQL filter applied on every read path.
In crates/ai-memory-store/src/reader.rs, all retrieval queries include this clause:
WHERE (pages.expires_at IS NULL OR pages.expires_at > :now)
This means:
- Before expiry: The page appears in
memory_queryresults normally - After expiry: The page is excluded from results as if it doesn't exist
- The markdown file remains on disk — only query visibility is affected
# Query before expiry — page is returned
mcp memory_query '{"query":"Project Plan"}'
# → contains the page
# Same query after 2027-01-01 — nothing returned
mcp memory_query '{"query":"Project Plan"}'
# → empty result set
This soft-expiry approach allows administrators to recover accidentally expired content before the sweep runs.
The Forget Sweep: Hard Deletion of Expired Pages
The memory_forget_sweep job (M8 retention pass) performs the actual cleanup. Running daily by default, this server-side job follows a documented two-phase process.
Phase 1: Tombstoning
Per docs/design-decisions.md line 162:
"Decay/forget runs as a separate
memory_forget_sweepjob: applies the retention formula; removes the Markdown source while tombstoning viais_latest=false+superseded_at"
This creates an audit trail before physical deletion.
Phase 2: Ancestry Purge
After a configured grace period, the sweep hard-deletes the tombstone's full version ancestry. The architecture documentation in docs/ARCHITECTURE.md line 368 confirms this: "purge aged tombstone ancestry, and hard-delete TTL-expired pages."
The sweep implementation resides in crates/ai-memory-mcp/src/server.rs within the memory_forget_sweep function.
# Preview what the sweep would delete (dry run)
mcp memory_forget_sweep '{"dry_run":true}'
# → lists pages with elapsed expires_at timestamps
# Execute permanent deletion
mcp memory_forget_sweep '{}'
# → markdown file removed, database records purged
TTL Outranks pinned: Priority Rules
A critical edge case: expiration overrides pinning. The usage documentation in docs/usage.md explicitly states that "TTL outranks pinned" — a page marked as pinned: true with an elapsed expires_at timestamp is still deleted by the forget sweep.
This prioritization prevents accidental data retention through conflicting flags.
Complete Page Lifecycle Diagram
| Stage | Trigger | State | User Visibility |
|---|---|---|---|
| Write | memory_write_page with expires_at |
Active, stored | Full query visibility |
| Soft Expiry | Current time > expires_at |
Filtered | Hidden from queries, file exists |
| Tombstone | memory_forget_sweep detects expiry |
is_latest=false |
Recoverable via history |
| Hard Delete | Grace period elapsed | Fully purged | Permanent removal |
Summary
memory_write_pagestoresexpires_atas UTC timestamp in the pages table (expires_atcolumn)- Read paths in
crates/ai-memory-store/src/reader.rsfilter withexpires_at IS NULL OR expires_at > :now memory_forget_sweepruns daily, tombstones expired pages, then hard-deletes after grace period- TTL takes precedence over the
pinnedflag — expired pinned pages are still removed
Frequently Asked Questions
What happens if I query a page after expires_at but before the sweep runs?
The page is hidden from results but recoverable. The SQL filter in the reader removes it from query output, yet the markdown file and database record remain intact until memory_forget_sweep executes its tombstoning and deletion phases.
Can I extend or remove expires_at after writing a page?
Yes. Since expires_at is front-matter metadata, calling memory_write_page again with an updated or omitted expires_at overwrites the stored timestamp, effectively resetting or removing the TTL constraint.
Does memory_forget_sweep only delete expired pages?
No. The sweep handles multiple retention policies including the M8 formula for version history. However, TTL-expired pages receive immediate hard-deletion priority alongside the standard tombstone-ancestry purge described in docs/ARCHITECTURE.md.
What timestamp format does expires_at require?
RFC 3339 format with explicit timezone, such as "2026-12-31T23:59:59Z". The system parses this via jiff::Timestamp, so offsets like +00:00 are also accepted but UTC (Z) is recommended for clarity.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →