Run uvicorn directly under the dev wrapper so a worker crash cannot hide behind a live reload parent. Preserve Python source reloads, restart isolated crashes with bounded diagnostics, and keep persistent crash loops loud.
Closes#1690.
In dev there is no supervisor: concurrently's --kill-others-on-fail tears
the whole stack down the moment uvicorn exits, the cause scrolls away with
the terminal, and the browser tab just says it can't reach the backend —
which is exactly how #1164 arrived with zero diagnostics.
- scripts/dev-backend.mjs: dev:api now runs uvicorn through a wrapper
(command args byte-identical, stdio inherited). On a non-Ctrl+C, non-zero
exit it prints a boxed banner: exit code/signal, the last 20 lines of
omnivoice.log (data dir resolved exactly like backend/core/config.py),
an OOM hint (SIGKILL/137 + the Linux journalctl -k check), and a pointer
to the crash notice the run sentinel raises on the next backend start.
Exits with the child's own code so --kill-others-on-fail still works.
Verified live: started the dev backend, SIGKILLed it, banner printed
with the real log tail and exit code 137.
- docs-sync: troubleshooting.md gains §14c (browser/dev/Docker crash
forensics: the mode-aware error, the dev banner, run_sentinel.json /
last_run_crash.json / GET /system/last-run-crash, cap+ack+version-gate
semantics) and §14's crash-notice blockquote no longer implies the
notice is desktop-only; CONTRIBUTING.md documents the dev:api wrapper.
- CHANGELOG.md: [Unreleased] entry for the #1164 class fix.
Tests: tests/frontend/devBackend.test.mjs (5) — the uvicorn args are
pinned byte-identical, data-dir resolution mirrors config.py, tail/banner
content incl. the OOM shapes.