register_watch previously relied entirely on tab-status (OSC 21337 /
cc-status) transitions. Sessions that report no machine-readable status
(plain TUIs, coding agents like Codex that don't emit status, bare
shells) produced no transitions, so the orchestrator had no event
channel and was reduced to polling get_screen_contents in a loop,
junking the chat with "still busy, checking again" turns.
When the target session reports no status, doRegisterWatch now creates a
.screenPoll watcher driven by ScreenWatchPoller: a headless
AIConversation that reads the rendered screen, judges doneness from
on-screen content (activity indicator vs ready prompt, not screen
stability, since a silently churning agent leaves the screen static),
and fires the same status_update a tab-status watcher would. The agent's
turn still ends immediately; no polling noise reaches the chat.
Details:
- Screen stability is not doneness: the model is handed the first
capture plus the two most recent so it can see whether an indicator is
still animating across frames.
- Per-target detection: idle/waiting/working each spell out their own
positive on-screen evidence (an indicator means reached for working
but not-yet for idle), instead of one finished-centric description.
- Unparseable replies get one rephrase pass, then fall back to unknown,
which the loop treats like not-yet so it never fires a false positive.
- Quadratic backoff delay(n)=3+n*n between polls, hard 5-minute cap,
then a watchTimedOut status_update so the agent can re-register.
- Watchers carry a mode; .screenPoll pollers are cancelled on
unregister/terminate/teardown and restarted after relaunch. The
tab-status handler is scoped to .tabStatus watchers.
- Poll lifecycle and model verdicts are traced via DLog.