Repository navigation
Description
Description
Replacing a cache entry with a larger value can make InMemoryCacheProvider undercount its stored bytes and exceed max_size_bytes, even when each individual value is smaller than the limit.
set() subtracts the old entry's size but leaves that entry in _cache while calling _evict_if_needed(). If the old entry has the earliest expiry, eviction finds it and subtracts its size a second time. Other entries then remain cached even though they should have been evicted. Removing the remaining entries can leave _current_size_bytes negative.
In the example below, a 20-byte cache holds values estimated at 24 bytes but reports 16. Expected behavior is to retain the replacement, evict newer, and return to zero tracked bytes when emptied.
Code Sample
import asyncio
from unittest.mock import patch
from agent_framework_purview._cache import InMemoryCacheProvider
async def main():
cache = InMemoryCacheProvider(max_size_bytes=20)
with patch("agent_framework_purview._cache.time.time", return_value=1000.0):
# JSON string quotes add two bytes to the estimated size.
await cache.set("older", "a" * 6, ttl_seconds=10) # 8 bytes
await cache.set("newer", "b" * 6, ttl_seconds=20) # 8 bytes
await cache.set("older", "c" * 14, ttl_seconds=30) # 16 bytes
print(await cache.get("newer"))
print(cache._current_size_bytes)
print(sum(entry[2] for entry in cache._cache.values()))
await cache.remove("older")
await cache.remove("newer")
print(cache._current_size_bytes)
asyncio.run(main())Error Messages / Stack Traces
No exception is raised. Actual output:
bbbbbb
16
24
-8
Expected output:
None
16
16
0Package Versions
Editable source checkout at e9857bd75a21843803431a2e7dfa41ee9b017ac7: agent-framework-core 1.20.0 and agent-framework-purview 1.0.0b261002.
Python Version
Python 3.12.14 on Linux.
Additional Context
The default ScopedContentProcessor uses this cache implementation. I am working on a focused fix that removes the old mapping before eviction, so its heap entry is treated as stale, with a regression test for replacement at capacity. The existing 17 cache tests pass before the change but do not cover this interaction.
Investigated and tested with Codex assistance. The reproduction is entirely local.
Metadata
Metadata
Labels
Type
Projects
- StatusShow more project fieldsDone
Activity
github-actions commentedon Oct 2, 2026
🤖 Automated triage reproduction notes (agent-authored — trust but verify)
Agent analysis
The bug reproduces in
python/packages/purview/agent_framework_purview/_cache.py::InMemoryCacheProvider.setwhen the earliest-expiring key is replaced by a larger value while near capacity, causing_evict_if_needed()to deduct the still-mapped old entry twice. Minimal repro: freeze time, place two 8-byte entries in a 20-byte cache, replace the earlier-expiring entry with a 16-byte value, and assert the other entry is evicted and tracked size returns to zero after removal.python/packages/purview/tests/purview/test_cache.py::TestInMemoryCacheProvider::test_growing_entry_at_capacity_maintains_size_and_evicts_other_entryagent-framework-purview 1.0.0b261002agent-framework-purview 1.0.0b261002dhairyajangir commentedon Oct 28, 2026
Eduard van Valkenburg (@eavanvalkenburg) Following up on #8988: the fix and regression test are in this PR, and the automated review on head db8cc2e reported no findings. The Python test matrix and code-quality checks passed.
The merge gate is still failing on team_check and command_check; the corresponding checks from the original review run were cancelled. Could you please check whether those prerequisites and the merge gate can be rerun, and review the PR when you have a chance? Thanks!
Python: Fix Purview cache size accounting when replacing entries (mic…