- event_generator now yields {event: 'ping', data: ''} on KEEPALIVE_TIMEOUT
instead of silently looping. Gives SSE clients stream-level liveness signal.
- _collect_events helper now enforces its timeout parameter via asyncio.wait_for,
preventing tests from hanging indefinitely if generator never yields.
Three bugs causing 100% CPU and container crash-looping in production:
1. sse-starlette ping=0 causes await anyio.sleep(0) busy loop in _ping task.
Each SSE connection spins a ping task at 100% CPU. Changed to ping=15
(built-in keepalive). Removed our manual ping yield in favor of continue.
2. Dockerfile purged curl after installing deno, but Docker healthcheck
(and compose override) uses curl. Healthcheck always failed -> autoheal
restarted the container every ~2 minutes. Keep curl in the image.
3. Downloads that fail during server shutdown leave zombie jobs stuck in
queued/downloading status (event loop closes before error handler can
update DB). Added startup recovery that marks these as failed.