From 371cfbc054fe995d9d1c42a5028e64aa2b8a98b4 Mon Sep 17 00:00:00 2001 From: 35C4n0r Date: Sun, 24 May 2026 09:46:27 +0530 Subject: [PATCH] fix(sweeper): key threshold cache by TTLPolicy value instead of id() - id(policy) returns the CPython memory address, which is reused as soon as the previous policy object is freed; within a single _plan() loop a policy_resolver that creates a fresh TTLPolicy per thread can produce two different policies with the same id(), causing the cache to return thresholds computed for the wrong policy - TTLPolicy is a frozen dataclass so it has __hash__ and __eq__ based on its fields; using the policy object itself as the dict key is correct and safe regardless of object identity --- src/langgraph_ephemeral_checkpointer/sweeper.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/langgraph_ephemeral_checkpointer/sweeper.py b/src/langgraph_ephemeral_checkpointer/sweeper.py index 793075e..12c5409 100644 --- a/src/langgraph_ephemeral_checkpointer/sweeper.py +++ b/src/langgraph_ephemeral_checkpointer/sweeper.py @@ -262,16 +262,14 @@ def _plan( dry_ids: list[str] = [] to_delete: list[_DeleteItem] = [] - # Threshold UUIDs are computed once per distinct policy object, not once per thread. - _cache: dict[int, tuple[str | None, str | None]] = {} + _cache: dict[TTLPolicy, tuple[str | None, str | None]] = {} def _thresholds(policy: TTLPolicy) -> tuple[str | None, str | None]: - pid = id(policy) - if pid not in _cache: + if policy not in _cache: idle = unix_to_uuid6(now - policy.idle_ttl_seconds) if policy.idle_ttl_seconds is not None else None age = unix_to_uuid6(now - policy.hard_age_ttl_seconds) if policy.hard_age_ttl_seconds is not None else None - _cache[pid] = (idle, age) - return _cache[pid] + _cache[policy] = (idle, age) + return _cache[policy] for tid, ts in timestamps.items(): policy = self._resolve_policy(tid)