canvas: poll-storm, SSE self-heal & keyboardless-wall UX (#141 #137 #142)#144
Merged
Conversation
Server: memoize the /agents payload for ~2s so N canvases x M panes stop each forking `tmux list-panes` + a `capture-pane` per claude pane on a short timer — a burst of client polls now collapses to one host sweep. Client: poll /agents fast only while the agent-tree pill is expanded and someone is watching states change; drop to a slow heartbeat when collapsed (still keeps the "who needs me" pill's dot/count live and discovers new agents). Gate the periodic controller poll (~2s), the beat pump (~1s) and the video-restore timer (~5s) on !document.hidden the way pollAgents already is, and run the beat pump only while the voice is speaking instead of forever.
Server: emit the heartbeat as a real `data: {"kind":"ping"}` frame
instead of an SSE `: comment` — EventSource ignores comments, so a
comment heartbeat never reaches the client and a silent stall (mobile
backgrounding, half-open TCP on a days-long wall) goes undetected. Cap
held-open /events streams in Hub.attach() (MAX_SSE_CLIENTS=64) and return
503 past the cap so a flood of half-open clients can't exhaust
ThreadingHTTPServer's threads/fds.
Client: ignore kind==='ping' for rendering but stamp lastEventTs on every
frame; a 15s watchdog closes and reopens the EventSource after ~45s of
silence so a stalled wall reconnects itself instead of only dimming an
8px dot. Reset the clock on foreground so unhiding doesn't trip a false
reconnect.
#142) Replace the native window.prompt() fallbacks (askToken / typedSeek / typedOpen) with an in-page input sheet reusing the #help/#toast/#peek overlay pattern, so they honour the e-ink theme and aren't dead modals on a keyboardless wall. On a /input 401, toast a pointer to the pairing QR (a 40-char token is misery to type on a wall) instead of popping a native modal. Make disconnect room-legible: after ~10s down, grey the canvas and float a big "reconnecting…" banner — coordinated with the #137 watchdog (shared setDisconnected) rather than a competing mechanism, so a frozen wall no longer looks live behind only an 8px dot. Route /input failures through toast(r.detail) instead of console.warn.
Owner
Author
|
Reopened and retargeted to |
This was referenced Jul 11, 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.
Three follow-up fixes from the canvas review, each a separate commit, all confined to
packages/visual/src/agent_media_visual/canvas.py. Branched from and targetingagent-tree-vim-nav.#141 — /agents poll storm (
perf)/agentspayload for ~2s (_agents_payload(), canvas.py:383) behind a lock, so a burst of client polls collapses to a singletmux list-panes+ per-panecapture-panesweep instead of N screens × M panes forking every few seconds./agentshandler now just returns the cached payload (canvas.py:2487)./agentspoll is now adaptive (scheduleAgents, canvas.py:2421) — fast (4s) only while the tree pill is expanded, a slow 12s heartbeat when collapsed (still keeps the "who needs me" pill's dot/count live and discovers new agents), 30s while backgrounded. Gated the controller poll (~2s), the beat pump (~1s) and the video-restore timer (~5s) on!document.hidden; the beat pump (pumpSeq, canvas.py:1597) now runs only while the voice is speaking instead of ticking forever.#137 — SSE self-heal + client cap (
fix)data: {"kind":"ping"}frame (canvas.py:2564) instead of an SSE: comment(whichEventSourceignores, so it could never drive a client watchdog). Added aMAX_SSE_CLIENTS=64cap inHub.attach()(canvas.py:66) — returnsNonepast the cap and the SSE handler answers503before sending headers (canvas.py:2543), so a flood of half-open/eventsstreams can't exhaust the threaded server's threads/fds.onmessageignoreskind==='ping'for rendering but stampslastEventTson every frame; a 15s watchdog closes and reopens theEventSourceafter ~45s of silence (connectEvents, canvas.py:1739) so a stalled wall reconnects itself instead of only dimming the 8px dot.lastEventTsis reset on foreground so unhiding doesn't trip a false reconnect.#142 — keyboardless-wall UX (
fix)window.prompt()fallbacks (askToken/typedSeek/typedOpen) with an in-page input sheet (askSheet, canvas.py:1908;#sheet, canvas.py:1305) reusing the#help/#toast/#peekoverlay pattern — respects the e-ink theme, not a dead modal. No nativeprompt()calls remain./input401,toast()a pointer to the pairing QR (…/pair) instead of a native modal — typing a 40-char token on a wall is misery; the in-page sheet is still offered for keyboard canvases.backdrop-filter, no conflict with the.layerfilters) and float a big "reconnecting…" banner (#offbar, canvas.py:1301). Driven by a sharedsetDisconnected(canvas.py:1728) that the canvas: SSE stream can silently stall (no heartbeat/watchdog) and has no client cap #137 watchdog andonopen/onerrorall call — one coherent mechanism, not two competing ones. The 10s escalation timer is not reset by repeatedonerror/retry, so a real outage actually surfaces./inputfailures now go throughtoast(r.detail)instead ofconsole.warn.The embedded client JS can't be exercised headless. It passes
node --check(syntax) and I've self-reviewed the logic, but the client-side behaviour needs your visual confirmation in a real browser / on the wall:s/o(typed seek / open URL) and the token/pairing path on a 401;#sheetand#offbaron the Pine Note.Verification
python3 -m py_compile packages/visual/src/agent_media_visual/canvas.py— passes.import agent_media_visual.canvas) — passes;MAX_SSE_CLIENTS=64,_agents_payloadpresent.node --check— passes (syntax only).Cleanup
Built in a leftover worktree at
~/projects/agent-media-canvas-followups(branchcanvas-review-followups) — remove it after merge:git worktree remove ~/projects/agent-media-canvas-followups.Closes #137
Closes #141
Closes #142