The heartbeat runner uses agent.write() (normal write), so heartbeat
ACK responses like "HEARTBEAT_OK" were not suppressed by the internal
run filter and leaked into the desktop UI chat. The Gateway path was
already fixed (Hub has delayed-start + isHeartbeatAckEvent filtering),
but the local Desktop path through AsyncAgent had no such filtering.
Add createFilteredHandler() to AsyncAgent that buffers message_start
for assistant messages and checks subsequent events with
isHeartbeatAckEvent(). Pure heartbeat ACKs are suppressed end-to-end;
all other messages are forwarded normally.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>