diff --git a/agent/conversation_loop.py b/agent/conversation_loop.py index d0e76d05df..2ab35ec945 100644 --- a/agent/conversation_loop.py +++ b/agent/conversation_loop.py @@ -182,6 +182,16 @@ _API_CALL_MODULES = frozenset({ }) +def _join_truncated_parts(parts: List[str]) -> str: + """Join continuation fragments, adding a newline where two would glue together (#78577).""" + joined = "" + for part in parts: + if joined and not joined[-1].isspace() and part and not part[0].isspace(): + joined += "\n" + joined += part + return joined + + def _apply_active_turn_redirect(agent: Any, messages: List[Dict[str, Any]], text: str) -> None: """Append a provider-safe checkpoint and correction to the live turn. @@ -3291,7 +3301,7 @@ def run_conversation( _retry.restart_with_length_continuation = True break - partial_response = agent._strip_think_blocks("".join(truncated_response_parts)).strip() + partial_response = agent._strip_think_blocks(_join_truncated_parts(truncated_response_parts)).strip() if partial_response: agent._vprint( f"{agent.log_prefix}⚠️ Response still truncated " @@ -7199,7 +7209,7 @@ def run_conversation( codex_ack_continuations = 0 if truncated_response_parts: - final_response = "".join(truncated_response_parts) + final_response + final_response = _join_truncated_parts([*truncated_response_parts, final_response]) truncated_response_parts = [] length_continue_retries = 0 # The continuation recovered, so the fragments stay in the transcript. diff --git a/tests/run_agent/test_continuation_ceiling_wedge.py b/tests/run_agent/test_continuation_ceiling_wedge.py index 96fd63e945..a1fa8c676f 100644 --- a/tests/run_agent/test_continuation_ceiling_wedge.py +++ b/tests/run_agent/test_continuation_ceiling_wedge.py @@ -229,3 +229,24 @@ class TestContinuationCeilingWedge: ) assert "second turn partial" in result2["final_response"] assert "and the rest." in result2["final_response"] + + +class TestTruncatedPartJoining: + """#78577 — parts joined with no separator glued text together.""" + + def test_glued_parts_get_a_newline(self): + from agent.conversation_loop import _join_truncated_parts + assert _join_truncated_parts( + ["Edited index.html", "Review the 5 changes"] + ) == "Edited index.html\nReview the 5 changes" + + def test_existing_whitespace_is_not_doubled(self): + from agent.conversation_loop import _join_truncated_parts + assert _join_truncated_parts(["line one\n", "line two"]) == "line one\nline two" + assert _join_truncated_parts(["word", " next"]) == "word next" + + def test_degenerate_inputs(self): + from agent.conversation_loop import _join_truncated_parts + assert _join_truncated_parts([]) == "" + assert _join_truncated_parts(["only"]) == "only" + assert _join_truncated_parts(["a", "", "b"]) == "a\nb"