Skip to content

fix(playground): daemon-gate wedges on hidden-tab first mount#404

Open
ljagiello wants to merge 1 commit into
mainfrom
fix/daemon-health-hidden-tab-wedge
Open

fix(playground): daemon-gate wedges on hidden-tab first mount#404
ljagiello wants to merge 1 commit into
mainfrom
fix/daemon-health-hidden-tab-wedge

Conversation

@ljagiello
Copy link
Copy Markdown
Contributor

@ljagiello ljagiello commented May 20, 2026

🪷 Koan

The tab opens.
No one is looking.
The probe never runs.

Summary

tick() bailed when document.visibilityState !== "visible", leaving the daemon-gate's loading branch mounted forever on any tab that mounted while hidden: state.loading only flips inside check()'s finally, so no probe → no flip → gate stuck on "CONNECTING…" indefinitely.

Affected tabs:

  • Background-opened tabs — 5s+ visible wedge until the next scheduled tick lands on a visible state.
  • DevTools-Protocol-driven tabs (Claude in Chrome, headless test harnesses) where `visibilityState` never transitions to `"visible"` — permanent wedge with the daemon perfectly healthy.

Fix

Drop the bail. That's it. One `if` block + a comment removed.

The bail's stated rationale ("Chrome throttles network in hidden tabs hard enough that a probe is more likely to time out") doesn't survive close inspection: Chrome throttles timers, not fetch; the steady cadence is 30s; the daemon is `localhost`, not a metered network. One extra `/api/daemon/health` request per 30s on a hidden tab is negligible.

Why not the visibilitychange-listener approach

An earlier version of this PR (f6e41fb1, force-pushed) preserved the bail and compensated with an unconditional first-probe path plus a visibilitychange listener (~40 lines). That worked but introduced a real race in scheduleNext — two check().finally(scheduleNext) chains could overlap and leak setTimeout handles. Reviewer (#404 review doc) called out that dropping the bail collapses the patch and eliminates the race surface entirely. This version is that collapse.

Test plan

Verified in MCP-driven Chrome (`visibilityState === "hidden"`):

  • Page renders the workspace inspector within ~600ms of first mount. Sidebar shows Online. `.gate-state` opacity transitions to 0 immediately after first probe lands.
  • Run-button flow completes end-to-end (modal opens, trigger fires, navigation to spawned session view).
  • `deno task typecheck` — 0 errors, 33 pre-existing warnings unchanged.

Without the fix, the same MCP tab wedges on `CONNECTING…` indefinitely (reproducible across hard reloads).

Independent of and orthogonal to PR #402 (Run-button `?nowait=true`); together they make MCP-driven QA of any signal-trigger feature actually feasible.

@ljagiello ljagiello requested a review from Vpr99 as a code owner May 20, 2026 00:34
`tick()` bailed when `document.visibilityState !== "visible"`, leaving
the daemon-gate's loading branch mounted forever on any tab that
mounted while hidden: `state.loading` only flips inside `check()`'s
`finally`, so no probe → no flip → gate stuck on "CONNECTING…"
indefinitely.

Affected tabs:
- Background-opened tabs (5s+ visible wedge until next scheduled tick
  lands on a visible state)
- DevTools-Protocol-driven tabs (Claude in Chrome, headless test
  harnesses) where `visibilityState` never transitions to "visible" —
  permanent wedge with the daemon perfectly healthy

The bail's stated rationale ("Chrome throttles network in hidden
tabs") doesn't survive close inspection: Chrome throttles timers, not
fetch; the steady cadence is 30s; the daemon is `localhost`, not a
metered network. One extra `/api/daemon/health` request per 30s on a
hidden tab is negligible.

The first attempt at this PR (commit f6e41fb) compensated for the
bail by adding an unconditional first-probe path and a
`visibilitychange` listener. That worked but introduced a race in
`scheduleNext` (two `check().finally(scheduleNext)` chains could
overlap and leak a `setTimeout` handle) and added ~40 lines of
machinery to recover behavior the bail was suppressing. Dropping the
bail collapses the whole patch to a comment swap + one deleted
branch, with no race surface.

Verified in MCP-driven Chrome (`visibilityState === "hidden"`): page
renders the workspace inspector within ~600ms after first mount and
the Run-button flow completes end-to-end.
@ljagiello ljagiello force-pushed the fix/daemon-health-hidden-tab-wedge branch from f6e41fb to 5f7d8ea Compare May 20, 2026 00:47
@LissaGreense
Copy link
Copy Markdown
Contributor

LissaGreense commented May 21, 2026

Hey — did a QA pass on this and noticed the original wedge already looks fixed on main via #368's state.hasConnected && guard, so I compared the two approaches in a Claude-in-Chrome CDP tab (the permanently-hidden case).

Setup: deno task playground against a local daemon, fresh CDP tab, measured first-mount gate clear and steady-state probes. Also killed a mock daemon mid-session to measure detection latency.

main this PR
Gate clears on first mount ~1.2s ~1.2s
Probes while hidden, after connect 0 in 38s 1 at t=29s
Detects daemon crash while hidden never ~12s (measured); 5–35s range

Both fix the wedge, but main goes silent once connected — for MCP / Claude-in-Chrome the tab is permanently hidden, so we'd read frozen daemon state forever. This PR keeps probing. Cost is one localhost fetch per 30s in healthy state (5s while unhealthy), inFlight guard prevents overlap, basically free.

Net: genuinely useful for the CDP/MCP path — unblocks QA of signal-trigger features from Claude-in-Chrome without faking visibility.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants