This repository was archived by the owner on Jun 3, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 54
feat(lifecycle): add forget flag + TTL to v2 ingest API (#166) #228
Open
chiragkhatri19
wants to merge
6
commits into
XortexAI:main
Choose a base branch
from
chiragkhatri19:feat/lifecycle-forget-ttl
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
8553057
feat(lifecycle): add MemoryLifecycle schema and pure lifecycle functions
e921618
feat(lifecycle): add forget TTL setting and thread extra_metadata thr…
1698c9c
feat(v2/ingest): add forget flag to v2 ingest API and thread lifecycl…
485f733
feat(retrieval): filter expired forget records at read time in _searc…
9a6f8fe
test(lifecycle): add unit + integration tests for forget/TTL; fix con…
8ae9176
fix(lifecycle): patch UPDATE data-leak, sanitize extra_metadata, hard…
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| """ | ||
| Memory lifecycle — pure, deterministic helper functions for forget/TTL. | ||
|
|
||
| is_retrievable() is the single retrieval-time gate. build_lifecycle_metadata() | ||
| stamps the lifecycle fields onto a metadata dict for storage. | ||
|
|
||
| All functions are side-effect-free so they can be tested without live services. | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from datetime import datetime | ||
| from typing import Any, Dict, Mapping, Optional | ||
|
|
||
|
|
||
| def is_retrievable(metadata: Optional[Mapping[str, Any]], now: datetime) -> bool: | ||
| """Return True when a record should appear in retrieval results. | ||
|
|
||
| Rules (applied in order): | ||
| 1. ``lifecycle_state == "forgotten"`` → hidden (manual soft-forget). | ||
| 2. ``forget is True`` and ``expires_at`` is present and in the past → hidden (TTL expired). | ||
| 3. Everything else (including all legacy records with no lifecycle keys) → retrievable. | ||
|
|
||
| Missing keys default to the legacy-safe value so records stored before | ||
| lifecycle was introduced are never hidden. | ||
| """ | ||
| if not metadata: | ||
| return True | ||
|
|
||
| if metadata.get("lifecycle_state", "active") == "forgotten": | ||
| return False | ||
|
|
||
| if metadata.get("forget"): | ||
| expires_raw = metadata.get("expires_at") | ||
| if expires_raw: | ||
| try: | ||
| if isinstance(expires_raw, datetime): | ||
| expires_at = expires_raw | ||
| else: | ||
| expires_at = datetime.fromisoformat(str(expires_raw)) | ||
| # Make both sides timezone-aware or both naive for comparison | ||
| if expires_at.tzinfo is None and now.tzinfo is not None: | ||
| from datetime import timezone | ||
| expires_at = expires_at.replace(tzinfo=timezone.utc) | ||
| elif expires_at.tzinfo is not None and now.tzinfo is None: | ||
| expires_at = expires_at.replace(tzinfo=None) | ||
| if expires_at < now: | ||
| return False | ||
| except (ValueError, TypeError): | ||
| pass | ||
|
|
||
| return True | ||
|
|
||
|
|
||
| def build_lifecycle_metadata( | ||
| now: datetime, | ||
| ttl_days: float, | ||
| reason: Optional[str] = None, | ||
| ) -> Dict[str, Any]: | ||
| """Return the lifecycle metadata dict to merge onto a forget=true record. | ||
|
|
||
| Called once at v2 ingestion time when the caller sets ``forget=true``. | ||
| The result is stored as part of the vector record's metadata so the | ||
| retrieval-time filter can enforce the TTL without any background sweeper. | ||
| """ | ||
| from datetime import timedelta | ||
| expires_at = now + timedelta(days=ttl_days) | ||
| meta: Dict[str, Any] = { | ||
| "forget": True, | ||
| "expires_at": expires_at.isoformat(), | ||
| "lifecycle_state": "active", | ||
| "created_at": now.isoformat(), | ||
| "updated_at": now.isoformat(), | ||
| } | ||
| if reason: | ||
| meta["forget_reason"] = reason | ||
| return meta |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If
r.metadataisNone(which is possible in some backends or mock tests, and is now handled safely byis_retrievable), callingr.metadata.get("main_content", "")on line 525 will raise anAttributeError. We should make this access defensive.