Replace the fixed asyncio.sleep(0.5) with a wait on an asyncio.Event that
fires once all thread messages have arrived over the API. Every buffered
message is delivered whether it survives intact or gets clobbered, so counting
THREADMSG occurrences is a deterministic drain-complete signal with no arbitrary
sleep and no dependence on the fix being present.
The synchronous logging path holds the main-task recursion guard around
notify_listeners_, but the buffered drain in Logger::process_messages_ did not.
When a message logged from a non-main thread was drained on the main loop and a
listener (the API log forwarder, or a logger.on_message automation) logged again
on the main task, that re-entrant log reused the shared tx_buffer_ and the API
shared_write_buffer_ while they were still in use, corrupting the in-flight
message; on ESP32 this surfaced as a StoreProhibited panic in the API send path.
Hold a RecursionGuard on main_task_recursion_guard_ across the drain so re-entrant
main-task logs are dropped there too, mirroring the synchronous path.
Adds an integration test that drives the buffered drain with a re-entrant
on_message log and verifies the buffered messages are delivered uncorrupted.