diff --git a/api/compression_continuation.py b/api/compression_continuation.py index 57d69974..64bc7dca 100644 --- a/api/compression_continuation.py +++ b/api/compression_continuation.py @@ -10,6 +10,12 @@ from pathlib import Path logger = logging.getLogger(__name__) +def _is_local_interactive_persisted_source(source): + return isinstance(source, str) and source in { + "webui", "tui", "cli", "desktop", "acp", + } + + def durable_compression_continuation(session): """Return (sealed, resumable tip), without making a recovery write. @@ -49,7 +55,7 @@ def durable_compression_continuation(session): if not tip or tip == sid: return True, None child = db.get_session(tip) - if not child or child.get("source") not in {"webui", "tui", "cli"}: + if not child or not _is_local_interactive_persisted_source(child.get("source")): return True, None for row in (parent, child): if row.get("profile_name") not in (None, "", profile): diff --git a/tests/test_compressed_idle_resume.py b/tests/test_compressed_idle_resume.py index 3dbb0ae2..23cc1c3b 100644 --- a/tests/test_compressed_idle_resume.py +++ b/tests/test_compressed_idle_resume.py @@ -49,8 +49,7 @@ class Handler: pass -def test_stale_post_rejected_before_workspace_or_worker_mutation(lineage, monkeypatch): - _, session = lineage +def _assert_stale_post_rotation(session, monkeypatch, expected_continuation): monkeypatch.setattr(routes, "_agent_runtime_barrier_response", lambda **kw: None) monkeypatch.setattr(routes, "_get_or_materialize_session", lambda *a, **kw: session) monkeypatch.setattr(routes, "_get_active_profile_name", lambda: "default") @@ -61,7 +60,41 @@ def test_stale_post_rejected_before_workspace_or_worker_mutation(lineage, monkey assert h.status == 409 payload = json.loads(h.wfile.getvalue()) assert payload["code"] == "session_rotated" - assert payload["continuation_session_id"] == "idlechild" + assert payload["continuation_session_id"] == expected_continuation + + +def test_stale_post_rejected_before_workspace_or_worker_mutation(lineage, monkeypatch): + _, session = lineage + _assert_stale_post_rotation(session, monkeypatch, "idlechild") + + +@pytest.mark.parametrize("source", ["desktop", "acp"]) +def test_same_source_local_interactive_lineage_resumes(lineage, monkeypatch, source): + db, session = lineage + db._conn.execute( + "UPDATE sessions SET source=? WHERE id IN ('sealedparent', 'idlechild')", + (source,), + ) + db._conn.commit() + + assert routes._pre_compression_continuation_session_id(session) == "idlechild" + _assert_stale_post_rotation(session, monkeypatch, "idlechild") + + +@pytest.mark.parametrize( + "source", + ["cron", "webhook", "kanban", "tool", "subagent", "unknown-source"], +) +def test_noninteractive_lineage_is_not_resumable(lineage, monkeypatch, source): + db, session = lineage + db._conn.execute( + "UPDATE sessions SET source=? WHERE id IN ('sealedparent', 'idlechild')", + (source,), + ) + db._conn.commit() + + assert routes._pre_compression_continuation_session_id(session) is None + _assert_stale_post_rotation(session, monkeypatch, None) def test_idle_tip_accepts_normal_persistence_without_reopening_parent(lineage):