Files
hapi/android/core/protocol
bd3fb24811 feat(android): global connection and runner version-skew banners (extracted onto main) (#2205)
* feat(android): global connection and runner version-skew banners

Two Home-screen chrome banners mirroring the web reference banner stack:

- Global connection banner: folds the SSE engine's attributed failure
  notice (reconnectNoticeFailure, 4s grace) with the resync lifecycle
  into one notice - Syncing (300ms debounce, 10s auto-clear cap,
  depth-counted episodes) suppresses Reconnecting, which labels the
  outage reason (heartbeat timeout / visibility recovery / connect
  timeout / transport error with HTTP code / closed). The engine now
  attributes every attempt end into ConnectionState.lastFailure,
  cleared on the next successful handshake.
- Runner version-skew banner: capability-set comparison
  (RunnerSkew.REQUIRED_MACHINE_CAPABILITIES, not semver) flags runners
  missing agent-availability/cursor-chat-store-status; expanded card +
  minimized chip, snooze 1h, manual restart via new
  POST /api/machines/:id/restart-runner (restart gated on
  newer-binary-on-disk mtime drift AND supervised restart).

StoreSyncTargets grows an onResyncEvent lifecycle hook (default no-op)
feeding the syncing half; HomeScreen grows optional chrome params
(null hides each banner).

Declared divergences from web/src:
- Banners mount on Home only, not above every route: an open chat
  already surfaces its own pipe state ("Reconnecting... Tap to retry"),
  so the two never double up on one screen (web mounts App-wide).
- The web OfflineBanner is not ported: a native app has no browser
  online/offline event pair; hub reachability is expressed through the
  reconnecting banner.
- Minimize/snooze state is process-lifetime memory (web: sessionStorage
  - same session-scoped lifetime per platform); the snooze timer does
  not survive process death.
- Runner rows filter only on capability skew; the active filter lives
  upstream in MachineStore (web filters machine.active on its side).

Co-Authored-By: Claude Code <noreply@anthropic.com>

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* Fix codeless transport attribution and banner polish from review

- SseEngine: a codeless Failure (DNS/TLS/socket errors — the dominant
  mobile outage shape) now attributes TransportError like the web's
  unconditional onerror → transport-error mapping, instead of degrading
  to a "connection closed" label.
- ConnectionChrome: a resync restart while the syncing banner is
  already shown keeps it shown (web doStartSync reshuffles timers with
  isSyncing staying true — no blink); the web's first-connect/30s-from-
  background display gating is now a declared divergence.
- RunnerSkewBanner: the expanded card scrolls past a 320dp cap (web
  40vh) so many skewed hosts cannot push the session list off-screen;
  restart failures surface the hub body's parsed error field instead
  of raw embedded JSON; handoff-hint/tooltip omissions declared.

Co-Authored-By: Claude Code <noreply@anthropic.com>

via [HAPI](https://hapi.run)

Co-Authored-By: HAPI <noreply@hapi.run>

* Show the reconnecting banner for auth-side outages with no failure reason

Review finding (ConnectionState.kt): a paired hub starting with a stale
JWT whose token refresh keeps failing cycles SseEngine Backoff without
any transport attempt running, so lastFailure stays null — after the 4s
grace the notice flow emitted null and ConnectionChrome read "no
banner". Home could sit disconnected indefinitely with no notice.

Outage visibility now lives on its own marker, independent of the
optional reason: reconnectOutageNotice (was reconnectNoticeFailure)
emits OutageNotice(lastFailure) once the grace elapses — non-null even
when no attempt attributed a reason — and ConnectionChrome maps any
OutageNotice to Reconnecting(failure), so a reason-less outage renders
the plain "Reconnecting…" strip (web shows the banner on sseDisconnected
regardless of reason; the parenthetical label simply drops).

Coverage added per review:
- repeated null-token retries keep the notice visible (engine level);
- null-reason outage still emits a notice after the grace (state level);
- outage without a reason still shows the banner (chrome level);
- the notice hides while backgrounded and returns with the foreground.
Resync debounce/cap and runner-restart error paths were already covered
(a sync shorter than the debounce…, a stuck sync auto-clears…, restart
failure surfaces the message…).

Co-Authored-By: Claude Code <noreply@anthropic.com>

---------

Co-authored-by: HAPI <noreply@hapi.run>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-11-02 11:37:47 +08:00
..