mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-06 17:38:35 +00:00
fix(session): tolerate invalid idle-compaction timestamps
This commit is contained in:
parent
07c2677eed
commit
e633f867e8
@ -31,9 +31,19 @@ class AutoCompact:
|
|||||||
now: datetime | None = None) -> bool:
|
now: datetime | None = None) -> bool:
|
||||||
if self._ttl <= 0 or not ts:
|
if self._ttl <= 0 or not ts:
|
||||||
return False
|
return False
|
||||||
if isinstance(ts, str):
|
try:
|
||||||
ts = datetime.fromisoformat(ts)
|
if isinstance(ts, str):
|
||||||
return ((now or datetime.now()) - ts).total_seconds() >= self._ttl * 60
|
ts = datetime.fromisoformat(ts)
|
||||||
|
current = now or datetime.now()
|
||||||
|
if getattr(ts, "tzinfo", None) is not None or current.tzinfo is not None:
|
||||||
|
idle_seconds = current.timestamp() - ts.timestamp()
|
||||||
|
else:
|
||||||
|
idle_seconds = (current - ts).total_seconds()
|
||||||
|
except (OSError, OverflowError, TypeError, ValueError):
|
||||||
|
# list_sessions() forwards raw persisted metadata; an unusable value
|
||||||
|
# must not escape the idle scan and stop the agent loop.
|
||||||
|
return False
|
||||||
|
return idle_seconds >= self._ttl * 60
|
||||||
|
|
||||||
def _has_compactable_idle_tail(self, key: str) -> bool:
|
def _has_compactable_idle_tail(self, key: str) -> bool:
|
||||||
session = self.sessions.get_or_create(key)
|
session = self.sessions.get_or_create(key)
|
||||||
|
|||||||
@ -154,6 +154,26 @@ class TestIsExpired:
|
|||||||
now_over = datetime(2026, 1, 1, 10, 10, 0)
|
now_over = datetime(2026, 1, 1, 10, 10, 0)
|
||||||
assert ac._is_expired(ts, now=now_over) is True
|
assert ac._is_expired(ts, now=now_over) is True
|
||||||
|
|
||||||
|
def test_unparseable_string_timestamp_returns_false(self):
|
||||||
|
"""A persisted timestamp that no longer parses must not raise.
|
||||||
|
|
||||||
|
list_sessions() forwards the raw persisted updated_at string, and
|
||||||
|
SessionManager._load already tolerates a malformed value through its
|
||||||
|
recovery path. The idle scan must mirror that tolerance instead of crashing.
|
||||||
|
"""
|
||||||
|
ac = _make_autocompact(ttl=15)
|
||||||
|
assert ac._is_expired("not-a-timestamp") is False
|
||||||
|
|
||||||
|
def test_tz_aware_string_timestamp_is_compared_by_instant(self):
|
||||||
|
"""A valid timestamp with an offset remains eligible for expiry."""
|
||||||
|
ac = _make_autocompact(ttl=15)
|
||||||
|
now = datetime(2026, 1, 1, 12, 0, 0)
|
||||||
|
recent = (now - timedelta(minutes=10)).astimezone().isoformat()
|
||||||
|
expired = (now - timedelta(minutes=20)).astimezone().isoformat()
|
||||||
|
|
||||||
|
assert ac._is_expired(recent, now=now) is False
|
||||||
|
assert ac._is_expired(expired, now=now) is True
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# _format_summary
|
# _format_summary
|
||||||
@ -221,6 +241,36 @@ class TestCheckExpired:
|
|||||||
assert len(scheduled) == 1
|
assert len(scheduled) == 1
|
||||||
assert "cli:old" in ac._archiving
|
assert "cli:old" in ac._archiving
|
||||||
|
|
||||||
|
def test_unparseable_updated_at_does_not_stop_scan(self):
|
||||||
|
"""A malformed timestamp is skipped without hiding later sessions.
|
||||||
|
|
||||||
|
The idle scan runs from the agent loop's inbound-timeout branch, so a
|
||||||
|
raised exception here would tear down the loop. list_sessions() forwards
|
||||||
|
the raw string, so check_expired must tolerate it like SessionManager
|
||||||
|
does when loading.
|
||||||
|
"""
|
||||||
|
ac = _make_autocompact(ttl=15)
|
||||||
|
mock_sm = MagicMock(spec=SessionManager)
|
||||||
|
old_dt = datetime.now() - timedelta(minutes=20)
|
||||||
|
session = _make_session("cli:old", updated_at=old_dt)
|
||||||
|
_add_turns(session, 5)
|
||||||
|
mock_sm.list_sessions.return_value = [
|
||||||
|
{"key": "cli:corrupt", "updated_at": "not-a-timestamp"},
|
||||||
|
{"key": "cli:old", "updated_at": old_dt.isoformat()},
|
||||||
|
]
|
||||||
|
mock_sm.get_or_create.return_value = session
|
||||||
|
ac.sessions = mock_sm
|
||||||
|
scheduled = []
|
||||||
|
|
||||||
|
def scheduler(coro):
|
||||||
|
scheduled.append(coro)
|
||||||
|
coro.close()
|
||||||
|
|
||||||
|
ac.check_expired(scheduler, _runtime)
|
||||||
|
|
||||||
|
assert len(scheduled) == 1
|
||||||
|
assert ac._archiving == {"cli:old"}
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_runtime_is_captured_before_background_starts(self):
|
async def test_runtime_is_captured_before_background_starts(self):
|
||||||
ac = _make_autocompact(ttl=15)
|
ac = _make_autocompact(ttl=15)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user