diff --git a/agent/conversation_compression.py b/agent/conversation_compression.py index 52778a4e3f..a02c322d90 100644 --- a/agent/conversation_compression.py +++ b/agent/conversation_compression.py @@ -3611,19 +3611,27 @@ def compress_context( if not force and _automatic_compression_gate_blocks(agent, bypass_cooldown): return messages, _existing_system_prompt(agent, system_message) - # Lazy feasibility probe (~400ms cold) on first attempt, not __init__; it sets - # _compression_warning so status replay still surfaces the warning. Marked checked - # only after the probe completes (transient failures are swallowed inside). - if not getattr(agent, "_compression_feasibility_checked", False): - check_compression_model_feasibility(agent) - agent._compression_feasibility_checked = True _pre_msg_count = len(messages) # In-place keeps the SAME session_id (no rotation/child/renumber/re-sync). A # missing attribute must default True, not rotation, which can wedge sessions. in_place = bool(getattr(agent, "compression_in_place", True)) + # Announce BEFORE the lazy feasibility probe: its live catalog / provider lookups are + # network-bound (connect timeouts stack up through proxies), and until this status lands + # the Desktop working row is a bare spinner with no "Summarizing thread" label (#111294). lifecycle = _announce_compression_start( agent, message_count=_pre_msg_count, approx_tokens=approx_tokens, focus_topic=focus_topic, force=force ) + # Lazy feasibility probe (~400ms cold) on first attempt, not __init__; it sets + # _compression_warning so status replay still surfaces the warning. Marked checked + # only after the probe completes (transient failures are swallowed inside). A hard + # rejection propagates; retire the announced phase first so the client is not left compacting. + if not getattr(agent, "_compression_feasibility_checked", False): + try: + check_compression_model_feasibility(agent) + except Exception: + lifecycle.complete(force_terminal=True) + raise + agent._compression_feasibility_checked = True lease, _abort_prompt = _acquire_compression_lease( agent, commit_fence=commit_fence, lifecycle=lifecycle, system_message=system_message, approx_tokens=approx_tokens, attempt_started_at=attempt.started_at, diff --git a/tests/agent/test_413_compression.py b/tests/agent/test_413_compression.py index 464cef6c79..65a9c83067 100644 --- a/tests/agent/test_413_compression.py +++ b/tests/agent/test_413_compression.py @@ -499,6 +499,44 @@ class TestPreflightCompression: ("compacted", COMPACTION_DONE_STATUS), ] + def test_compress_context_announces_before_lazy_feasibility_probe(self, agent): + """The compacting status must land BEFORE the first-attempt feasibility probe (live catalog / + provider lookups): a slow probe otherwise leaves the Desktop working row on a bare spinner with no + \"Summarizing thread\" label (#111294). The probe's hard rejection still retires the phase.""" + import agent.conversation_compression as cc + + agent.compression_enabled = True + agent._compression_feasibility_checked = False + events = [] + agent.status_callback = lambda ev, msg: events.append((ev, msg)) + + def _fake_compress(messages, current_tokens=None, focus_topic=None, force=False, memory_context=""): + events.append(("compress", "started")) + return [{"role": "user", "content": f"{SUMMARY_PREFIX}\nPrevious conversation"}] + + with ( + patch.object(cc, "check_compression_model_feasibility", + side_effect=lambda a: events.append(("probe", "feasibility"))), + patch.object(agent.context_compressor, "compress", side_effect=_fake_compress), + patch.object(agent, "_build_system_prompt", return_value="new system prompt"), + patch("agent.conversation_compression.estimate_request_tokens_rough", return_value=42), + ): + agent._compress_context([{"role": "user", "content": "hello"}], "system prompt", approx_tokens=1234) + + assert events[:2] == [("lifecycle", COMPACTION_STATUS), ("probe", "feasibility")] + assert events[-1] == ("compacted", COMPACTION_DONE_STATUS) + assert agent._compression_feasibility_checked is True + + # Hard rejection (aux window below minimum) propagates AND retires the announced phase. + agent._compression_feasibility_checked = False + events.clear() + with ( + patch.object(cc, "check_compression_model_feasibility", side_effect=ValueError("aux too small")), + pytest.raises(ValueError), + ): + agent._compress_context([{"role": "user", "content": "hello"}], "system prompt", approx_tokens=1234) + assert events == [("lifecycle", COMPACTION_STATUS), ("compacted", COMPACTION_DONE_STATUS)] + def test_compress_context_emits_one_terminal_status_when_lock_is_unavailable(self, agent): """A rejected lock must retire the started desktop compaction phase.""" agent.compression_enabled = False