You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
TUI keeps audio output active after notification playback #54432
The TUI starts audio output for notification sounds but does not stop it when playback finishes.
On macOS, this prevents Bluetooth headphones from switching playback to another device. A local patch that stops output when no voices remain active fixes the problem. Other platforms have not been tested.
Thanks for the report! I was able to reproduce this. I don't have a macOS machine here, so I tested on Linux with PulseAudio, and it behaves the same way there.
Steps: turn on attention sounds (attention: { sound: true }), send a prompt and let the session finish so the "Session done" sound plays, then list the audio streams with pactl list short sink-inputs.
What happens: before the prompt there are no streams. After the sound plays, one stream from opencode is still open 15 seconds later, well after the ~1 s sound has ended. It stays open until opencode exits.
It still happens on the latest dev (0d61729b3). In packages/tui/src/audio.ts, play() starts the audio engine the first time a sound plays, but nothing stops it again. It is only released when the TUI shuts down. That fits your finding that stopping output once no voices are active fixes it.
The script below reproduces it. The open stream doesn't appear on screen, so the evidence is the pactl output the script logs; the recording only shows the session finishing.
Related: #35242 reported the same kind of leak in v1 (a notification sound kept a macOS output stream open and coreaudiod busy). It was closed for inactivity without a fix.
recording.mp4Reproduction script (drive.ts)
// Repro for #54432: after a TUI attention sound finishes, the audio output stream stays open.// Needs a PulseAudio server and `pactl`, e.g. on a headless Linux box:// apt-get install -y pulseaudio pulseaudio-utils && pulseaudio -D --exit-idle-time=-1// export PULSE_SERVER=unix:/tmp/pulse-<id>/native # so the TUI and pactl reach the same server// Then: opencode-drive run drive.ts (OC_DEV=/path/to/checkout to run from source)import{Effect}from"effect"import{execSync}from"node:child_process"import{Llm,OpenCodeDriver}from"opencode-drive"constsinkInputs=()=>{constout=execSync("pactl list short sink-inputs",{encoding: "utf8"}).trim()returnout ? out.split("\n") : []}exportdefaultOpenCodeDriver.use({
...(process.env.OC_DEV ? {opencode: {dev: process.env.OC_DEV}} : {}),tui: {recording: true},tuiConfig: {attention: {sound: true,volume: 1}},},({ ui, llm })=>Effect.gen(function*(){yield*Effect.log(`sink-inputs before: ${sinkInputs().length}`)yield*llm.queue(Llm.text("Done."))yield*ui.submit("say done")yield*ui.waitFor("Done.")// The "Session done" sound is ~1s long; wait well past the end of playback.for(constsof[2,5,10,15]){yield*Effect.sleep(s===2 ? "2 seconds" : s===5 ? "3 seconds" : "5 seconds")constinputs=sinkInputs()yield*Effect.log(`sink-inputs ${s}s after session done: ${inputs.length}${JSON.stringify(inputs)}`)}yield*ui.screenshot("after-sound")constremaining=sinkInputs().lengthif(remaining>0)yield*Effect.log(`BUG REPRODUCED: ${remaining} audio output stream(s) still open after playback finished`)elseyield*Effect.log("OK: no audio output stream left open")}),)
Run it with opencode-drive: opencode-drive run ./drive.ts. It drives opencode from your PATH; set OC_DEV to the path of an OpenCode checkout to run that from source instead.
Same here on v2.0.32 (macOS 27.0.1, MacBook Pro M4, Ghostty 1.3.1). Besides the Bluetooth switching, the open stream also costs CPU and blocks idle sleep on macOS, so here are some numbers, plus a test of #54434.
Measured with attention.sound: true, an idle TUI and a local fake model (CPU as % of one core, over 20–30 s):
No TUI, or a freshly started TUI before any sound has played: coreaudiod 0.0–0.1%, no assertions.
One TUI after a single "Session done" sound: coreaudiod 8.5–8.8%, steady for as long as the TUI sits idle. pmset -g assertions shows two PreventUserIdleSystemSleep assertions held by coreaudiod (an AudioTap and BuiltInSpeakerDevice).
Three TUIs, one sound each: coreaudiod 11.6%, three assertions.
After quitting the TUI: back to ~0% within seconds, assertions gone.
Same test with attention.sound: false: coreaudiod stays at 0.0%.
The TUI process itself stays at ~1% in every case, so the cost is all in coreaudiod.
This matches what I saw in daily use: with a few TUIs left open after agent runs, the Mac carried extra load until I quit them. Since a fresh TUI stays at 0% until its first sound, #56188 (closed as a duplicate of this one) fits the same cause.
Tested #54434: ran the PR head (5b13af4) and its base (3c61484) from source with the same script, two turns each, each turn playing the "Session done" sound:
Base: the stream opens when the sound plays and never closes; coreaudiod 8.3% while idle, two assertions held after both turns.
PR: the stream opens when the sound plays and is released within a few seconds (assertions gone by ~5 s); coreaudiod 0.0% while idle after each turn. The second sound restarts the engine as expected.
bun test test/audio-idle.test.ts in packages/tui: 2 pass.
So #54434 fixes the CPU and idle sleep side as well. Until it lands, attention.sound: false plus a TUI restart works around it.
Not tried: Bluetooth output (only the built-in speakers).
Activity
opencode-agent commentedon Oct 11, 2026
Thanks for the report! I was able to reproduce this. I don't have a macOS machine here, so I tested on Linux with PulseAudio, and it behaves the same way there.
Steps: turn on attention sounds (
attention: { sound: true }), send a prompt and let the session finish so the "Session done" sound plays, then list the audio streams withpactl list short sink-inputs.What happens: before the prompt there are no streams. After the sound plays, one stream from opencode is still open 15 seconds later, well after the ~1 s sound has ended. It stays open until opencode exits.
It still happens on the latest
dev(0d61729b3). Inpackages/tui/src/audio.ts,play()starts the audio engine the first time a sound plays, but nothing stops it again. It is only released when the TUI shuts down. That fits your finding that stopping output once no voices are active fixes it.The script below reproduces it. The open stream doesn't appear on screen, so the evidence is the
pactloutput the script logs; the recording only shows the session finishing.Related: #35242 reported the same kind of leak in v1 (a notification sound kept a macOS output stream open and
coreaudiodbusy). It was closed for inactivity without a fix.recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.infinitr0us commentedon Oct 20, 2026
Same here on v2.0.32 (macOS 27.0.1, MacBook Pro M4, Ghostty 1.3.1). Besides the Bluetooth switching, the open stream also costs CPU and blocks idle sleep on macOS, so here are some numbers, plus a test of #54434.
Measured with
attention.sound: true, an idle TUI and a local fake model (CPU as % of one core, over 20–30 s):pmset -g assertionsshows twoPreventUserIdleSystemSleepassertions held by coreaudiod (an AudioTap and BuiltInSpeakerDevice).attention.sound: false: coreaudiod stays at 0.0%.This matches what I saw in daily use: with a few TUIs left open after agent runs, the Mac carried extra load until I quit them. Since a fresh TUI stays at 0% until its first sound, #56188 (closed as a duplicate of this one) fits the same cause.
Tested #54434: ran the PR head (5b13af4) and its base (3c61484) from source with the same script, two turns each, each turn playing the "Session done" sound:
bun test test/audio-idle.test.tsin packages/tui: 2 pass.So #54434 fixes the CPU and idle sleep side as well. Until it lands,
attention.sound: falseplus a TUI restart works around it.Not tried: Bluetooth output (only the built-in speakers).