Closed bug report

Shared player wedges silently with two desktops connected: unpinned drainer, ambiguous browser_open_window, no fail-and-advance

John Lauer · 2mo ago ·closed by John Lauer

Summary

On a machine with two Adom Desktops connected to the same relay (a laptop + a second box), the shared player wedges: clips synthesize and enqueue fine, but nothing is ever heard. status reports now playing "x.mp3" on :43073 at 0s / 0s indefinitely, the queue only grows, and check <clip> stays PLAYING (or QUEUED) forever. A ~10 second clip never reaches PLAYED. Observed live on adom-tts 1.9.29 while trying to narrate hands-free to a driving user, who heard nothing across roughly a dozen clips.

Root cause (fairly confident)

The drainer cannot open its player surface, and the failure is ambiguous-target, which the im-driving skill explicitly warns about:

If the relay has >1 client, the pup player's browser_open_window is AMBIGUOUS and fails silently. Pin it to the user's host: export ADOM_DESKTOP_TARGET=<host>.

The event log is unambiguous about the mechanism (~59 of these in one session):

open_surface_failed error="adom-desktop browser_open_window failed: " surface="pup"

The problem is whose environment the drainer runs in. The drainer is a long-lived background process. It was started earlier, without ADOM_DESKTOP_TARGET set. Exporting ADOM_DESKTOP_TARGET in the shell that later runs say does not reach the already-running drainer, so its browser_open_window calls stay unpinned, stay ambiguous with two desktops connected, and fail every time. No surface opens, so there is no audio sink, so clips sit "playing" at 0s/0s.

browser_list_windows from a pinned shell returned 5 sessions the whole time, so the bridge itself was up and answering. It is specifically the drainer's own unpinned surface-open that fails.

Three things that turn a transient failure into a stuck player

  1. A wedged clip at the head of the queue blocks everything behind it. Once one clip is stuck "playing" with no sink, every later clip is QUEUED behind it forever. A clip that cannot be sunk within N seconds should fail and advance, not block the queue.

  2. status reports it as actively playing. now playing "x" on :43073 at 0s / 0s reads as healthy while the audio position never advances. A 0s / 0s that never moves should surface as stalled, not as playing. Right now the only way to know it is dead is to notice the queue growing.

  3. reopen did not recover it, and killing the :43073 owner did not either, status came back reporting the same pid. Whatever keeps the drainer alive (auto-heal on status, a supervisor) resurrects it into the same broken, unpinned state rather than a fresh pinned one, so there was no clean way to reset it short of a host restart.

Also minor: adom-desktop status reports browser.bridgeRunning: false while browser_list_windows returns 5 live sessions, so that field is misleading during triage.

Repro

  1. Connect two Adom Desktops to the same relay (e.g. a laptop and a second machine).
  2. From a shell that has NOT exported ADOM_DESKTOP_TARGET, let the drainer start (any say --play).
  3. adom-tts say "test" --play --surface pup, later, even from a shell that DOES export ADOM_DESKTOP_TARGET.
  4. adom-tts status shows now playing ... stuck at 0s / 0s, queue grows, check never reaches PLAYED. adom-tts logs shows repeated open_surface_failed ... browser_open_window failed ... surface="pup".

Suggested fixes

  • Pin the drainer to a target at spawn, from ADOM_DESKTOP_TARGET (or an explicit --target), and re-resolve it when the environment changes, so its browser_open_window is never ambiguous when multiple desktops are connected. This is the core fix.
  • Fail-and-advance instead of block: a clip that cannot open a surface (or cannot start audio) within a short deadline should be marked failed and the queue should move on, so one bad clip cannot silence the whole session.
  • Honest status: 0s / 0s that does not advance for more than a few seconds should read as stalled/no sink, not playing.
  • A real reset path: a stop/restart that fully tears down and respawns the drainer pinned and clean (today reopen re-offers the tab but keeps the broken player; killing :43073 respawned the same broken pid).
  • Fix the misleading browser.bridgeRunning: false when the bridge is actually answering.

Impact

Hands-free is exactly the situation where the user cannot look at the screen, so a silent failure that still reports "playing" is the worst possible mode: the person is told nothing and hears nothing. The feature and the HD-side lightbulb prompt are fine; the shared player's surface-open and stall handling are what need hardening.

Environment: adom-tts 1.9.29, two Adom Desktops on one relay (laptop AdomLapper + ConfRoomROG), user in pup window mode, HD 0.1.223 / AD 1.9.165.

2 Replies

John Lauer · 2mo ago

Fixed in 1.9.30 (published; all container copies byte-synced). Every point addressed:

1. Core — pin the drainer's target. ADOM_DESKTOP_TARGET is now persisted to ~/.adom/tts/env.json by any healthy caller and adopted by the drainer, AND the pup surface-open resolves the target fresh on every call (live env → env.json → the sole connected desktop) and passes --target. So a long-lived drainer that started unpinned now picks up a target that appears later, without a restart. When >1 desktop is connected and none is pinned, browser_open_window no longer fails with an empty string — it fails with an explicit message: AMBIGUOUS pup target: 2 Adom Desktops connected and no ADOM_DESKTOP_TARGET set. Pin it… then adom-tts restart.

2. Fail-and-advance, never wedge. If the surface open itself keeps erroring (ambiguous target, dead bridge) — never a poll, never a successful open — wait_for_audience gives up (~45s, then ~8s once it knows the surface is down) and the drainer marks the clip surface-unavailable into History and advances. No retry (retrying a broken surface just re-wedges). One un-openable clip can no longer silence the whole session. check reports it as FAILED (surface-unavailable). Verified live on a 2-desktop rig: surface_giveup waited_s=45 → clip_end verdict=surface-unavailable → queue drained.

3. Honest status. A clip sitting at 0s / 0s with no live page (poll_age > 5s) now reads ⚠ STALLED … no audio sink (player surface not open, Ns silent) with the fix inline, instead of a healthy-looking now playing.

4. A real reset — adom-tts restart. Kills the drainer, clears the lock + portfile, and respawns a fresh drainer of the same surface (reads --surface pup off the dead one's cmdline so a pup session doesn't come back as hydrogen), pinned from the current env. reopen only re-offered the tab and kept the broken player; restart replaces it. Queued clips survive.

Recovery, when wedged: export ADOM_DESKTOP_TARGET=<host> && adom-tts restart (or just set it before the first say --play). Both skills (user + dev tiers) now document the multi-desktop pinning requirement.

5. browser.bridgeRunning: false while the bridge answers — that field belongs to adom-desktop, not adom-tts, so it's out of scope here; worth a one-line issue on the adom-desktop page. adom-tts no longer relies on it (it probes adom-desktop targets directly).

Leaving this open for you to confirm on the real ConfRoomROG + AdomLapper rig — my verification was in an isolated home (the ambiguous-open and fail-and-advance paths exercise fully there, but a successful pinned pup open needs your hardware). Close it once you've heard a clip land.

John Lauer · 1mo ago

Closing: every failure mode in this report has a shipped fix and a regression test, and the report itself predates the naming cutover (browser_open_window is now pup_open_window, ADOM_DESKTOP_TARGET is now ADOM_BRIDGE_TARGET with the old name read as a fallback, and the CLI is adom-bridge). Current state, 1.9.45:

  • Target pinning: ADOM_BRIDGE_TARGET is persisted and resolved fresh on every open (env, then env.json, then the sole connected machine); with more than one machine and no pin the error is explicit rather than silent.
  • Fail and advance: an unopenable surface gives up in about 45s (8s once known down), marks the clip surface-unavailable into History, and advances. One bad surface can no longer wedge the queue for everyone.
  • Honest status: 0s / 0s with no live page reports STALLED, no audio sink instead of "now playing".
  • Port stability: the drainer reuses its previous port across restarts (port_reused in the log), so an open page never strands on a dead port; restart also no longer wipes the portfile.
  • Actionable failure: surface-unavailable names the live player-page URL and the working hydrogen surface; adom-tts config separates the service endpoint from the player page URL.
  • Self-surfacing: adom-tts reopen re-opens whichever surface the drainer is on (a pup drainer gets a pup window), bound to the live port.
  • Root cause of "pup never plays" found later: a fully occluded pup window has its media pipeline suspended by Chrome; the pup player window now opens visible.

Regression coverage: tools/ralph-test.sh T9 (port stability) plus the fail-and-advance path, 11/11 green. The one item I cannot claim from the container is a live two-machine pup run on ConfRoomROG plus AdomLapper; both are on the relay again as of today, so if the wedge recurs on real hardware, reopen this with the adom-tts logs tail and I will pick it up.

Log in to reply.