Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions gateway/stream_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@ async def _send_fallback_final(self, text: str) -> None:
safe_limit = max(500, raw_limit - 100)
chunks = self._split_text_chunks(continuation, safe_limit)

stale_message_id = self._message_id # partial message to clean up
last_message_id: Optional[str] = None
last_successful_chunk = ""
sent_any_chunk = False
Expand Down Expand Up @@ -687,6 +688,22 @@ async def _send_fallback_final(self, text: str) -> None:
# so any stale tool-progress bubble gets closed off.
self._notify_new_message()

# Remove the frozen partial message so the user only sees the
# complete fallback response. Best-effort — if the platform doesn't
# implement ``delete_message``, the delete fails (flood control still
# active, bot lacks permission, message too old to delete), the
# partial remains but at least the full answer was delivered.
if stale_message_id and stale_message_id != last_message_id:
delete_fn = getattr(self.adapter, "delete_message", None)
if delete_fn is not None:
try:
await delete_fn(self.chat_id, stale_message_id)
except Exception as e:
logger.debug(
"Fallback partial cleanup failed (%s): %s",
stale_message_id, e,
)

self._message_id = last_message_id
self._already_sent = True
self._final_response_sent = True
Expand Down
1 change: 1 addition & 0 deletions scripts/release.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"128259593+Gutslabs@users.noreply.github.com": "Gutslabs",
"50326054+nocturnum91@users.noreply.github.com": "nocturnum91",
"223003280+Abd0r@users.noreply.github.com": "Abd0r",
"HuangYuChuh@users.noreply.github.com": "HuangYuChuh",
"ra2157218@gmail.com": "Abd0r",
"abdielv@proton.me": "AJV20",
"mason@growagainorchids.com": "masonjames",
Expand Down
73 changes: 73 additions & 0 deletions tests/gateway/test_stream_consumer.py
Original file line number Diff line number Diff line change
Expand Up @@ -793,6 +793,79 @@ async def test_fallback_final_sends_full_text_at_tool_boundary(self):
"_send_fallback_final — the #10807 fix should prevent this"
)

@pytest.mark.asyncio
async def test_fallback_final_deletes_partial_after_chunks_succeed(self):
"""After fallback chunks land, the frozen partial must be deleted so
the user sees only the complete response (#16668)."""
adapter = MagicMock()
adapter.send = AsyncMock(
return_value=SimpleNamespace(success=True, message_id="msg_new"),
)
adapter.edit_message = AsyncMock(
return_value=SimpleNamespace(success=True),
)
adapter.delete_message = AsyncMock(return_value=None)
adapter.MAX_MESSAGE_LENGTH = 4096

config = StreamConsumerConfig(edit_interval=0.01, buffer_threshold=5)
consumer = GatewayStreamConsumer(adapter, "chat_123", config)

# Seed the consumer as if it already edited a partial message that
# later got stuck (flood control etc.) — _message_id is the stale id.
consumer._message_id = "msg_partial"
consumer._last_sent_text = "Working on i"

await consumer._send_fallback_final("Working on it. Done!")

adapter.delete_message.assert_awaited_once_with("chat_123", "msg_partial")
assert consumer._final_response_sent is True

@pytest.mark.asyncio
async def test_fallback_final_does_not_delete_when_no_chunks_reach_user(self):
"""If every fallback send fails, the partial is the only thing the
user has — must NOT be deleted."""
adapter = MagicMock()
adapter.send = AsyncMock(
return_value=SimpleNamespace(success=False, error="network down"),
)
adapter.edit_message = AsyncMock(
return_value=SimpleNamespace(success=True),
)
adapter.delete_message = AsyncMock(return_value=None)
adapter.MAX_MESSAGE_LENGTH = 4096

config = StreamConsumerConfig(edit_interval=0.01, buffer_threshold=5)
consumer = GatewayStreamConsumer(adapter, "chat_123", config)

consumer._message_id = "msg_partial"
consumer._last_sent_text = "Working on i"

await consumer._send_fallback_final("Working on it. Done!")

adapter.delete_message.assert_not_awaited()

@pytest.mark.asyncio
async def test_fallback_final_skips_delete_when_adapter_lacks_method(self):
"""Platforms without delete_message must not crash the fallback path."""
adapter = MagicMock(spec=["send", "edit_message", "MAX_MESSAGE_LENGTH"])
adapter.send = AsyncMock(
return_value=SimpleNamespace(success=True, message_id="msg_new"),
)
adapter.edit_message = AsyncMock(
return_value=SimpleNamespace(success=True),
)
adapter.MAX_MESSAGE_LENGTH = 4096

config = StreamConsumerConfig(edit_interval=0.01, buffer_threshold=5)
consumer = GatewayStreamConsumer(adapter, "chat_123", config)

consumer._message_id = "msg_partial"
consumer._last_sent_text = "Working on i"

# Should not raise even though the adapter has no delete_message.
await consumer._send_fallback_final("Working on it. Done!")
assert consumer._final_response_sent is True


class TestInterimCommentaryMessages:
@pytest.mark.asyncio
Expand Down
Loading