`heartbeat_ack` is discarded when a script run starts before the queue flushes
Checklist
- I have searched the existing issues for similar issues.
- I added a very descriptive title to this issue.
- I have provided sufficient information below to help reproduce this issue.
Summary
ForwardMsgQueue.clear(retain_lifecycle_msgs=True) keeps only new_session, script_finished, session_status_changed, parent_message, page_info_changed and toast deltas (forward_msg_queue.py lines 135-146). heartbeat_ack is not in that set, so an ack that is already sitting in the queue is destroyed instead of delivered. AppSession._clear_queue calls this on every SCRIPT_STARTED, and the runtime flushes queues on a short timer, so an ack enqueued in the window between a heartbeat arriving and the next script run starting never reaches the browser. A host that sends one SEND_APP_HEARTBEAT and waits for the ack can wait forever.
Reproducible Code Example
from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
from streamlit.runtime.forward_msg_queue import ForwardMsgQueue
queue = ForwardMsgQueue()
ack = ForwardMsg()
ack.heartbeat_ack = True
finished = ForwardMsg()
finished.script_finished = ForwardMsg.FINISHED_SUCCESSFULLY
queue.enqueue(ack)
queue.enqueue(finished)
[m.WhichOneof("type") for m in queue._queue] # ['heartbeat_ack', 'script_finished']
queue.clear(retain_lifecycle_msgs=True)
[m.WhichOneof("type") for m in queue._queue] # ['script_finished'] <- ack is goneSteps To Reproduce
streamlit run(oruv run streamlit run) the snippet.- Observe that the second print no longer contains
heartbeat_ack.
End to end: open an app in an iframe host that sends SEND_APP_HEARTBEAT exactly once while a rerun is being triggered (for example, immediately after a widget interaction). The ack for that heartbeat is never received.
Expected Behavior
A heartbeat ack is a one-shot reply to a client request, like a toast is a one-shot notification. It should survive the interrupted-run queue clear, or be sent directly rather than queued, so that one heartbeat always produces one ack.
Current Behavior
The ack is silently removed from the queue when a script run starts before the queue is flushed. Nothing is logged. The client sees no reply.
Is this a regression?
- Yes, this used to work in a previous version.
Debug info
- Streamlit version: 1.63.0 and 1.64.0, both reproduced
- Python version: 3.14.6
- Operating System: Linux Mint 22.3 Zena 64-bit
- Browser: Version 152.0.7977.82 (Official Build) for Linux Mint (x86_64) but not browser-specific; reproduced as a pure function of
ForwardMsgQueue
Additional Information
The toast case (#7740) was fixed by adding _is_toast_delta(msg) to the retain predicate (forward_msg_queue.py line 146); the same shape would fix this (msg.WhichOneof("type") == "heartbeat_ack"). We hit this from a test harness that uses a heartbeat round trip to prove a WebSocket reconnect completed; sending heartbeats on an interval rather than once is the workaround.
Source: streamlit/streamlit