Skip to content

feat(memory): an end user's retention sweep deletes what is older than their retention_days (PRD-274 Wave 3) - #1488

Merged
daarthur merged 1 commit into
mainfrom
afp/w3-g-retention-sweep
Nov 11, 2026
Merged

daarthur merged 1 commit into
mainfrom
afp/w3-g-retention-sweep

Conversation

@daarthur

@daarthur daarthur commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

What and why

PRD-274 §5.5 says retention is set per end user, and #1479's consent record carries retention_days: "Days their memories are kept; none: until erased". Until now nothing enforced it. This is the first of two stacked PRs:

  1. This PR: the sweep, which deletes what is older than each end user's retention.
  2. Running it daily on the shared scheduler, and updating PRD-274 (feat(memory): the end-user retention sweep runs daily on the shared scheduler (PRD-274 Wave 3) #1489).

Nothing calls the sweep until PR 2, so this PR changes no behaviour on its own.

services/end_user_retention.sweep_end_user_retention deletes, for every end user with retention_days and no erase started:

  • their facts made before the cutoff. The new DurableMemoryStore.prune_older_than(namespace, cutoff) reads their one namespace in pages straight from Qdrant and deletes the old points by id.
    • It reads straight from Qdrant so that neither the run's reader nor the retired filter (_scroll's decorators) leaves an old fact behind, retired ones included.
    • created_at has only a keyword index, so the age is checked in Python, as the archival job does. A fact with no readable created_at is kept.
  • their messages sent before the cutoff, in their own conversations (chats.end_user_id, feat(chat): a server key's chat turn for one of its end users (PRD-274 Wave 3) #1486).
  • then each of their conversations left empty that was itself opened before the cutoff. A chat just opened has no message for a moment, so it isn't deleted.

Its behaviour:

  • An end user whose erase has started is left to the erase.
  • Running it twice deletes nothing more.
  • One end user that fails is logged and counted, and the rest are still swept.
  • Their Redis sessions need nothing: a conversation untouched for a day has none left at the default MEMORY_SESSION_TTL_SECONDS.

For Gerard to confirm (PR 2 records it as PRD-274 decision 22): retention covers their conversations as well as their facts. retention_days says "memories", but their messages are their own words too.

How it was verified

  • tests/test_afp_w3_end_user_retention.py (3 tests; real database, with a stand-in durable store for the sweep and a stand-in Qdrant client for the prune):

    • The store's prune, over two pages: it deletes the two old facts, one of them Z-suffixed, and keeps a recent one, an undated one and an unreadable one, all within the one namespace.
    • The sweep:
      • for an end user with 30 days, it prunes their namespace with a cutoff 30 days back;
      • it deletes a conversation whose messages are all old, and the old message in one that also has a recent one;
      • it keeps a conversation opened just now;
      • it leaves an end user with no retention, one being erased, and a key's untagged conversation alone.
    • An end user whose prune fails is counted, and their conversation is untouched, while another is swept.
  • Without the change, the test file can't import the new module.

  • Every test file touching the durable store, end users or retention passes: 32 files, 489 tests.

  • The code shape check has no errors, lint-imports is clean, and ruff is clean.

  • Tests added or updated, and they fail without this change

  • CI green (a lane marked non-required counts too)

  • Both editions considered: local (AUTH_EDITION=local) and hosted (the same code; with no durable store configured the prune deletes nothing)

  • UI change: screenshots below (none)

AI assistance

  • None
  • Assisted: tool(s): Claude Code; it wrote or changed: the prune, the sweep and the tests
  • I have read and understood every line of this change, and can explain it in review

Checklist (AGENTS.md)

  • Every commit signed off (git commit -s)
  • No secrets, customer data or credentials anywhere in the change
  • Settings through orchestrator/config.py; no hardcoded values
  • Replaced paths deleted in this pull request (no shims or V2 copies)
  • Migration (if any): one head, chained onto the current head, safe on a create_all-built database, head pins moved (none)
  • New routes the frontend calls are in orchestrator/reports/route-manifest.json (none)
  • Docs updated where behaviour changed (PR 2 updates PRD-274 when the sweep runs)

🤖 Generated with Claude Code

…n their retention_days (PRD-274 Wave 3)

services/end_user_retention.sweep_end_user_retention, for every end user with
retention_days and no erase started:

- their facts made before the cutoff (DurableMemoryStore.prune_older_than, read
  in pages straight from the store, so retired facts go too);
- their messages sent before it, in their own conversations, and then each of
  their conversations left empty that was itself opened before it.

Running it twice deletes nothing more; one end user that fails is logged and
counted, and the rest are swept. Nothing schedules it yet (the next PR).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Dave Arthur <davidarthur@hotmail.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

@coderabbitai

coderabbitai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor
Pre-merge checks: Inconclusive 2

Failed Failed checks: 2 inconclusive

Check name Status Explanation Resolution
Title check Pending This assessment has not completed. The check will be retried on the next pre-merge check run.
Description check Pending This assessment has not completed. The check will be retried on the next pre-merge check run.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@daarthur
daarthur merged commit b68525c into main Nov 11, 2026
55 of 57 checks passed
@daarthur
daarthur deleted the afp/w3-g-retention-sweep branch November 11, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant