-
-
Notifications
You must be signed in to change notification settings - Fork 3
fix: batch dedupe must ignore TTL expiry (zero-loss); scope depends_on edges on task create (security) #190
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -741,6 +741,7 @@ def _load_active_rows( | |
| project: str | None = None, | ||
| search_agents: list[str] | None = None, | ||
| now: float | None = None, | ||
| include_expired: bool = False, | ||
| ): | ||
| """Fetch active (non-superseded) rows, scoped by project/agent. | ||
|
|
||
|
|
@@ -754,6 +755,12 @@ def _load_active_rows( | |
| Non-numeric or missing ``forget_after`` values are silently ignored so | ||
| existing memories are never affected. ``now`` defaults to | ||
| ``time.time()``; pass an explicit float in tests to control the clock. | ||
|
|
||
| ``include_expired=True`` skips the TTL filter only: superseded rows | ||
| (``valid_to`` set) stay excluded, but rows hidden purely because their | ||
| ``forget_after`` has passed are returned. Batch dedupe uses this so an | ||
| expired-but-still-present id is not re-ingested (see | ||
| ``existing_source_ids``). Recall paths never pass it. | ||
| """ | ||
| if now is None: | ||
| now = time.time() | ||
|
|
@@ -782,21 +789,22 @@ def _load_active_rows( | |
| # nest the caller's per-item metadata under ``meta["metadata"]``. | ||
| # Tolerating both shapes makes forget_after expire identically no | ||
| # matter which ingest path wrote the row. Top level wins if set. | ||
| fa = meta.get("forget_after") | ||
| if fa is None: | ||
| inner = meta.get("metadata") | ||
| if isinstance(inner, dict): | ||
| fa = inner.get("forget_after") | ||
| if fa is not None: | ||
| try: | ||
| if float(fa) < now: | ||
| continue | ||
| except (TypeError, ValueError): | ||
| logger.debug( | ||
| "ignore non-numeric forget_after=%r on row id=%s", | ||
| fa, | ||
| row["id"], | ||
| ) | ||
| if not include_expired: | ||
| fa = meta.get("forget_after") | ||
| if fa is None: | ||
| inner = meta.get("metadata") | ||
| if isinstance(inner, dict): | ||
| fa = inner.get("forget_after") | ||
| if fa is not None: | ||
| try: | ||
| if float(fa) < now: | ||
| continue | ||
| except (TypeError, ValueError): | ||
| logger.debug( | ||
| "ignore non-numeric forget_after=%r on row id=%s", | ||
| fa, | ||
| row["id"], | ||
| ) | ||
|
|
||
| if project is not None or search_agents is not None: | ||
| # Project filter: skip rows positively tagged with a different | ||
|
|
@@ -821,9 +829,18 @@ def existing_source_ids(self, agent: str | None = None) -> set[str]: | |
| whose ``id`` is already present are skipped instead of duplicated. | ||
| Rows tagged with a different agent are excluded when ``agent`` is set; | ||
| untagged rows are included, matching the search-scoping rules. | ||
|
|
||
| Dedup must consider every physically-present row, including rows whose | ||
| ``forget_after`` has passed (``include_expired=True``): a TTL-expired | ||
| row is only hidden from recall, it still exists on disk, so re-ingesting | ||
| its id would write a second archive/vector row every re-POST (zero-loss | ||
| violation). Superseded rows (``valid_to`` set) are genuinely removed and | ||
| stay excluded, so re-importing intentionally-cleared content re-adds it. | ||
| """ | ||
| out: set[str] = set() | ||
| for row in self._load_active_rows(search_agents=[agent] if agent else None): | ||
| for row in self._load_active_rows( | ||
| search_agents=[agent] if agent else None, include_expired=True | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. SUGGESTION: Re-POST with a refreshed (future) Because Reply with |
||
| ): | ||
| try: | ||
| meta = json.loads(row["metadata_json"]) | ||
| except (json.JSONDecodeError, TypeError): | ||
|
|
||
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.
SUGGESTION: Two issues remain in this per-dependency loop (carried from the prior review; the tokenless scope bug it raised was fixed by keying on
token_project).dep_idtriggers its owntask_projects([dep_id, dep_id])round-trip (see_enforce_edge_project_scopeat line 1494). The sibling edge endpoints pass both ids to a singletask_projects(...)call; collapsing the wholedepends_onlist into one lookup (then checking each result) avoids O(N) DB round-trips.depends_onis only checked to be alist(line 1354), not that its elements are strings. A non-string element is passed straight into_enforce_edge_project_scope(which expectsstr) and, sincetask_projectswon't match a non-string key, yields a misleading 403 ("task not available in the token's project scope") instead of a 400. Validateisinstance(dep_id, str)and raise_BadRequest.Reply with
@kilocode-bot fix itto have Kilo Code address this issue.