Repository navigation
fix: an open dashboard tab must stay a watcher, or a headless run hangs - #46
Merged
Merged
Conversation
The standalone server registered a watcher when an SSE client connected and then pinged every 25s, but the ping never refreshed the flag — against a 15s TTL that left an open tab a watcher 60% of the time. An ask raised in the 10s gap was delegated to the composer panel, and with no UI tab attached there is no panel: the turn hung until the answer timeout and the run never left `research`. Measured on a real headless run (2026-10-07) against a live plugin: the ask `approval/asked` was in the session log and the SSE connection was ESTABLISHED, while `GET /api/state` reported `pending: []` for 10+ minutes and every settle POST answered 409. The ping is now the heartbeat it was always meant to be — it calls `noteWatcher()` — and `SSE_KEEPALIVE_MS` moves into `src/watcher-ttl.ts` beside the TTL it must stay under, so the interval cannot drift outside the window it refreshes. TTL 15s -> 60s, which clears the 25s ping with room for a slow read. Verified: the new test fails on the old code (no `noteWatcher` in the handler) and on a 25s TTL (the arithmetic), and passes on the fix; full suite 820 pass / 0 fail; `tsc --noEmit` clean.
This was referenced Oct 7, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the bug that hung the first headless run of this loop: an open dashboard tab was a watcher only 60% of the time, so an ask raised in the gap was delegated to the composer panel — which does not exist on a headless run, so the turn hung until the answer timeout.
The measurement
A real headless run on 2026-10-07 (harness 0.2.0-rc.2,
fl-liveprofile, plugin v0.4.3) reached step 1, calledbash, and stopped:approval/askedfor thatbashcall,lsofshowed the SSE connection to:8121ESTABLISHED,GET /api/statereportedpending: []for 10+ minutes, with every settle POST answering409 no such pending approval.That is the claim never being made. Reading the code:
openSsecallsnoteWatcher()once on connect, thensetInterval(… , KEEPALIVE_MS)writes: pingevery 25s — without callingnoteWatcheragain — againstWATCHER_TTL_MS = 15_000. So the tab was a watcher for 15s out of every 25s, andcreateApprovalRegistry'sif (!hasWatcher()) return next()handed the ask to the composer.The fix
noteWatcher().SSE_KEEPALIVE_MSmoves out ofsrc/dashboard.tsintosrc/watcher-ttl.ts, beside the TTL it must stay under, so the interval cannot drift outside the window it refreshes.WATCHER_TTL_MS15s → 60s, which clears the 25s ping with room for a slow remote read.Verification
The new test fails on the old code — revert only the
noteWatcher()call in the keep-alive handler andtest/dashboard.test.tsfails onnot ok 41; set the TTL to 25s and it fails on the arithmetic (the SSE ping every 25000ms cannot keep a 25000ms watcher TTL alive). Both confirmed, then restored.Full suite: 820 pass / 0 fail / 1 skipped (821 tests);
tsc --noEmitclean;bash .githooks/pre-commit --all→security scan ok (all).End-to-end on a real headless run against this build: the same profile, plugin installed from this worktree, headless Chrome, dashboard as the approval channel. The first ask appeared in
pendingand settled200within ~5s of being raised — 19 claims, zero 409s — and the run walked the whole spine:outcome: goal-met, 24 steps, $0.1264. The test phase passed after twotest → implementbounces, and ship produced a local commit (af0c144, no remote configured — the workspace is a scratch repo on purpose).Not claimed
The run's own
verify.shgate passes (OK: landing page present, uses assistant-ui), butnext buildfails on the artifact it produced:app/layout.tsximports./globals.cssand no CSS file was ever written. That is a model defect the gate did not catch, not a plugin bug — the gate asserts the page exists and uses assistant-ui, which it does. Widening the gate is a separate change and is not in this PR.