Self-review (3-agent + codex) findings on the async QueueListener change:
1. (HIGH) The os._exit shutdown backstop called flush_log_queue(), whose
stop() joins the listener thread unbounded. If that thread is wedged on
the rotation lock — the exact failure this change survives — shutdown
re-freezes. Add drain_log_queue(timeout): stop-only, bounded via a
throwaway joiner thread. Also release PID/runtime locks BEFORE the drain
so a slow drain can't strand them.
2. (MED) _log_queue/_queue_listener/_queued_file_handlers were read-modify-
written without a lock across register/stop/flush/reset; a gateway-init
race with a plugin/CLI path could leave two live listeners. Guard all
four globals with a single _queue_state_lock.
3. (MED) _NonFormattingQueueHandler.prepare() enqueued the same LogRecord a
synchronous handler on the emitting thread may still format/mutate.
Return copy.copy(record) (preserves msg/args/exc_info for deferred
RedactingFormatter) to remove the cross-thread mutation race.
E2E-verified: bounded drain returns in ~500ms on a permanently-wedged
listener; 4x20 concurrent flushes single-listener no-crash; args still
format and secrets still redact through the copied record.