The CI harness exports PI_TEST_RUNTIME=1, which the wrapper subprocess
inherited; isBunTestRuntime() then suppressed unref in the worker client
and deterministically kept the wrapper alive until the test timed out.
Override PI_TEST_RUNTIME=0 in the wrapper env and restore the 10s bound.
- Updated sdk-tool-activation expectations for 386385f18b: sessions
without a granted write tool keep extension/SDK tools top-level and
allocate no xd:// state instead of auto-granting write.
- Raised the unref'd-worker parent-exit repro to a 30s timeout; the 10s
ceiling SIGTERMed the wrapper (exit 143) on shared-core CI runners.
Bun keeps the parent event loop alive when an unref'd child has a piped
stderr stream. The first #4324 fix started with stderr: "pipe", so a
long-lived idle TTS/STT/tiny/mnemopi worker could keep short CLI commands
alive even after proc.unref().
Switch worker stderr capture to a temp-file fd target instead of a Bun
ReadableStream pipe. The parent does not start any JS read while the
worker is alive; after onExit it reads the bounded tail from the file,
logs captured lines, appends the tail to the surfaced worker Error, then
closes and removes the temp capture.
Add a regression that spawns a non-test wrapper process with an idle
unref'd worker and asserts the wrapper exits immediately. Existing stderr
capture, truncation, and intentional SIGKILL behavior remain covered.
Fixes#4324
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