Fact: _serve_dual_port (tinyagentos/main.py:103) drives the main app and the browser-proxy origin with asyncio.gather(main_server.serve(), proxy_server.serve()). When the main app's lifespan raises during startup, uvicorn logs "Application startup failed. Exiting." but Server.serve() RETURNS instead of raising. gather then waits on the proxy server forever.
Consequence: the process stays alive listening only on the proxy port, systemd reports the unit active, Restart=always never triggers, and the controller is silently dead on :6969. Observed live on a beta install: 2.5 hours of "active (running)" with zero listeners on the main port. A startup crash should be a crash.
Fix: replace the bare gather with FIRST_COMPLETED semantics: when either server's serve() completes, cancel and await the other; exit non-zero if the main server never reached started (so systemd surfaces the failure and Restart= does its job). Factor the wait logic into a small helper testable with stub servers.
Fact:
_serve_dual_port(tinyagentos/main.py:103) drives the main app and the browser-proxy origin withasyncio.gather(main_server.serve(), proxy_server.serve()). When the main app's lifespan raises during startup, uvicorn logs "Application startup failed. Exiting." butServer.serve()RETURNS instead of raising. gather then waits on the proxy server forever.Consequence: the process stays alive listening only on the proxy port, systemd reports the unit active, Restart=always never triggers, and the controller is silently dead on :6969. Observed live on a beta install: 2.5 hours of "active (running)" with zero listeners on the main port. A startup crash should be a crash.
Fix: replace the bare gather with FIRST_COMPLETED semantics: when either server's serve() completes, cancel and await the other; exit non-zero if the main server never reached started (so systemd surfaces the failure and Restart= does its job). Factor the wait logic into a small helper testable with stub servers.