Inference worker subprocesses (TTS, STT, tiny-model, mnemopi embeddings)
were spawned with stderr: "ignore", so a native crash inside the child
was completely discarded. The parent only ever logged the bare exit code
(e.g. Kokoro TTS's recurring "tts subprocess exited with code 7"),
leaving the recurring crash loop undiagnosable.
createWorkerSubprocess now pipes stderr and drains it in the parent:
- Each decoded stderr line is forwarded to logger.debug under
"<exitLabel> stderr" so operators get live visibility on chatty native
runtimes without touching the chat scrollback.
- A bounded 16 KiB ring keeps the tail of stderr so the eventual exit
Error carries the actual crash reason (ONNX Runtime traceback, glibc
assertion, etc.) instead of "code 7" alone. The prefix is preserved so
existing log grepping keeps working.
- The exit event and the stderr pipe are independent, so a synchronous
read in onExit would race the drain. SpawnedSubprocess grew a
stderrDrained: Promise<void>, and onExit chains the error surface off
it so callers see the whole tail. Tests can await stderrDrained
deterministically instead of racing wall-clock timers.
- Intentional terminate() SIGKILLs still stay silent — signal-exit
gating on intentionalExit is unchanged.
Fixes#4324