From 26b4a5816a78de7b5db72ab69a22e75114ea3b95 Mon Sep 17 00:00:00 2001 From: Anthony Ruiz Date: Sun, 20 Sep 2026 17:07:57 +0000 Subject: [PATCH] fix: retain legacy compression recovery on older Agent APIs --- api/compression_continuation.py | 11 +++++++++ tests/test_compressed_idle_resume.py | 37 ++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/api/compression_continuation.py b/api/compression_continuation.py index 410f2e66..57d69974 100644 --- a/api/compression_continuation.py +++ b/api/compression_continuation.py @@ -3,6 +3,7 @@ SQLite owns compression lineage; sidecar snapshot flags may predate a Desktop/CLI rotation. Never reopen a sealed parent or mutate Agent state here. """ +import inspect import logging from pathlib import Path @@ -30,6 +31,16 @@ def durable_compression_continuation(session): if not path.is_file(): return False, None db = SessionDB(path, read_only=True) + # Establish the complete read API before accepting SQLite authority. + # Old Agents must retain legacy sidecar recovery, not a sealed null tip. + for name in ('get_session', 'get_compression_tip'): + method = getattr(db, name, None) + if not callable(method): + return False, None + try: + inspect.signature(method).bind(sid) + except (TypeError, ValueError): + return False, None parent = db.get_session(sid) if not parent or parent.get("end_reason") != "compression": return False, None diff --git a/tests/test_compressed_idle_resume.py b/tests/test_compressed_idle_resume.py index a6da417e..3dbb0ae2 100644 --- a/tests/test_compressed_idle_resume.py +++ b/tests/test_compressed_idle_resume.py @@ -109,6 +109,43 @@ def test_noncompressed_parent_and_absent_database_keep_legacy_behavior(lineage, assert not (tmp_path / 'missing').exists() +@pytest.mark.parametrize('method', ['get_session', 'get_compression_tip']) +@pytest.mark.parametrize('kind', ['missing', 'noncallable', 'signature']) +def test_older_agent_keeps_sidecar_recovery(tmp_path, monkeypatch, method, kind): + import sys + from api.compression_continuation import durable_compression_continuation + + class OldDB: + closed = False + def __init__(self, *args, **kwargs): + pass + def get_session(self, sid): + return {'end_reason': 'compression'} + def get_compression_tip(self, sid): + return 'legacychild' + def close(self): + OldDB.closed = True + + if kind == 'missing': + delattr(OldDB, method) + elif kind == 'noncallable': + setattr(OldDB, method, None) + else: + setattr(OldDB, method, lambda self, sid, required: None) + monkeypatch.setitem(sys.modules, 'hermes_state', SimpleNamespace(SessionDB=OldDB)) + (tmp_path / 'state.db').touch() + monkeypatch.setattr(profiles, '_resolve_profile_home_for_name', lambda _: str(tmp_path)) + session = SimpleNamespace(session_id='legacyparent', profile='default', pre_compression_snapshot=True) + child = SimpleNamespace(session_id='legacychild', profile='default', + parent_session_id='legacyparent', pre_compression_snapshot=False, + updated_at=2, created_at=1) + monkeypatch.setattr(routes, 'SESSIONS', {'legacychild': child}) + monkeypatch.setattr(routes, 'SESSION_DIR', tmp_path) + assert durable_compression_continuation(session) == (False, None) + assert OldDB.closed + assert routes._pre_compression_continuation_session_id(session) == 'legacychild' + + def test_browser_rotation_restores_draft_without_reposting(): source = (Path(__file__).parents[1] / 'static/messages.js').read_text() helper = source[source.index('async function _recoverCompressedSend('):source.index('function _restoreComposerDraftAfterFailedSend(')]