fix(gateway): stop internal bookkeeping writes from advancing the session activity clock
set_session_metadata() and advance_compression_session()'s repoint both stamped entry.updated_at = now. updated_at is the user-activity clock that drives idle/daily reset policy and the restart-resume freshness gate (suspend_recently_active, #85709), so a background metadata write (e.g. Slack thread watermark) or a background compression repoint on a long-idle session could make it look freshly active and get it falsely resume_pending after a gateway restart. These are the last internal stamp sites after784f733cf(recover) and5462f689b(touch_activity gating): drop the stamps, keep the durable save. Follow-up to #85895 (closed) — credit @GodsBoy for the report-side push and @chelsealong for the analysis on #85709.
This commit is contained in:
@@ -2895,6 +2895,12 @@ class SessionStore:
|
||||
Values must be small and JSON-serializable — they are written into
|
||||
the routing index (state.db gateway_routing table + the legacy
|
||||
sessions.json mirror) so they survive gateway restarts.
|
||||
|
||||
Metadata writes are internal bookkeeping and deliberately do NOT
|
||||
advance ``updated_at``: it is the user-activity clock that drives
|
||||
idle/daily reset policy and the restart-resume freshness gate
|
||||
(#85709), and a background write must not make an idle session look
|
||||
fresh.
|
||||
"""
|
||||
with self._lock:
|
||||
self._ensure_loaded_locked()
|
||||
@@ -2902,7 +2908,6 @@ class SessionStore:
|
||||
if entry is None:
|
||||
return False
|
||||
entry.metadata[key] = value
|
||||
entry.updated_at = _now()
|
||||
self._save()
|
||||
return True
|
||||
|
||||
@@ -3349,7 +3354,10 @@ class SessionStore:
|
||||
target_session_id,
|
||||
):
|
||||
return None
|
||||
entry.updated_at = _now()
|
||||
# Compression repoint is store bookkeeping, not user activity —
|
||||
# leave ``updated_at`` alone so a background compression on an
|
||||
# idle session cannot make it look fresh to reset policy or the
|
||||
# restart-resume freshness gate (#85709).
|
||||
self._save()
|
||||
return entry
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import json
|
||||
import pytest
|
||||
from dataclasses import replace
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch, MagicMock
|
||||
from hermes_state import SessionDB
|
||||
@@ -1291,6 +1291,34 @@ class TestSessionMetadata:
|
||||
== "123.456"
|
||||
)
|
||||
|
||||
def test_metadata_write_does_not_touch_activity_clock(self, tmp_path):
|
||||
"""set_session_metadata is bookkeeping — it must not bump updated_at.
|
||||
|
||||
updated_at drives idle/daily reset policy and the restart-resume
|
||||
freshness gate (#85709); a background metadata write on an idle
|
||||
session must not make it look recently active.
|
||||
"""
|
||||
config = GatewayConfig()
|
||||
store = SessionStore(sessions_dir=tmp_path, config=config)
|
||||
store._db = None
|
||||
source = SessionSource(
|
||||
platform=Platform.SLACK,
|
||||
chat_id="C123",
|
||||
chat_type="group",
|
||||
user_id="U123",
|
||||
thread_id="123.000",
|
||||
)
|
||||
|
||||
entry = store.get_or_create_session(source)
|
||||
idle = datetime.now() - timedelta(days=21)
|
||||
with store._lock:
|
||||
entry.updated_at = idle
|
||||
|
||||
assert store.set_session_metadata(entry.session_key, "k", "v")
|
||||
assert entry.updated_at == idle
|
||||
# And the restart freshness gate must still see it as idle.
|
||||
assert store.suspend_recently_active(max_age_seconds=120) == 0
|
||||
|
||||
|
||||
class TestRewriteTranscriptPreservesReasoning:
|
||||
"""rewrite_transcript must not drop reasoning fields from SQLite."""
|
||||
|
||||
@@ -292,4 +292,25 @@ class TestAdvanceCompressionSession:
|
||||
db.end_session.assert_not_called()
|
||||
db.reopen_session.assert_not_called()
|
||||
|
||||
def test_repoint_does_not_touch_activity_clock(self, tmp_path):
|
||||
"""Compression repoint is bookkeeping — it must not bump updated_at.
|
||||
|
||||
A background compression on an idle session must not make it look
|
||||
fresh to reset policy or the restart-resume freshness gate (#85709).
|
||||
"""
|
||||
db = _db_returning({})
|
||||
store = _make_store_with_db(tmp_path, db)
|
||||
source = _source()
|
||||
key = store._generate_session_key(source)
|
||||
original = _make_entry(key, "sid_parent")
|
||||
idle = datetime.now() - timedelta(days=21)
|
||||
original.updated_at = idle
|
||||
store._entries[key] = original
|
||||
|
||||
result = store.advance_compression_session(key, "sid_parent", "sid_tip")
|
||||
|
||||
assert result is not None
|
||||
assert result.updated_at == idle
|
||||
assert store.suspend_recently_active(max_age_seconds=120) == 0
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user