LearnNewsExamplesServices
Frontmatter
id16699
titleCockpit fleet-connection UX: reconnect affordance + shell-aware offline guidance + auto-recheck
stateClosed
labels
enhancementai
assigneesneo-fable-clio
createdAtAug 8, 2026, 3:41 PM
updatedAtAug 8, 2026, 10:30 PM
githubUrlhttps://github.com/neomjs/neo/issues/16699
authorneo-fable-clio
commentsCount2
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 8, 2026, 10:30 PM

Cockpit fleet-connection UX: reconnect affordance + shell-aware offline guidance + auto-recheck

Closed Backlog/active-chunk-14 enhancementai
neo-fable-clio
neo-fable-clio commented on Aug 8, 2026, 3:41 PM

Provenance: operator UX request during the first live harness iteration loop (2026-08-08): "UX: how about a connect or reconnect button inside the harness?" — asked after the second consecutive run landed on the offline banner for a different root cause than the first. Both causes were correct fail-closed behavior; the product gave the operator no way to see which case they were in or to recover without restarting processes.

Context

The day's live-run sequence (receipts on #16693/#16694/PR #16696) produced three distinct offline-banner causes in three runs: (1) bearer non-coordination (fixed by #16694's self-supplied transport), (2) the UI-mode capability gate resolving null (same fix), (3) a stale manually-started transport squatting :8083 — the shell's foreign-listener branch correctly refused adoption, but the banner still said "start it: npm run ai:fleet-server", which in the shell topology is now actively wrong advice (the shell self-supplies; a manual start CAUSES the foreign-listener case).

The Problem

Three UX gaps, one surface:

  1. No recovery affordance. The offline state is computed at load; when the transport becomes reachable later (the #16694 spawn completing after first paint, a freed port, a restarted server), nothing re-queries. The only operator recovery is restarting the app.
  2. The banner's guidance is topology-blind. "start it: npm run ai:fleet-server" is right for the plain-browser flow and wrong inside the shell — there it should distinguish: transport starting (spawn in flight) vs another fleet server holds the port (foreign listener, with the probe's named refusal) vs transport stopped.
  3. No auto-recheck on late readiness. In the shell, main KNOWS the moment bootUiFleetTransport() resolves up: true — the renderer never hears about it.

The Architectural Reality

  • The offline state + banner live in the apps/agentos fleet views (the fail-closed routing matrices from the W2 arc; the banner is #15284's operable-cold surface).
  • The shell↔renderer channel exists: preload.cjs exposes IPC (fleet-request); a fleet-ready push (main → renderer) is the same trusted-sender pattern in the other direction, consumed via the preload allowlist. Browser flow has no such signal — the button covers it there.
  • bootUiFleetTransport() (PR #16696) returns {mode: 'reuse'|'spawn'|'foreign-listener', up} — exactly the states the banner should render.

Verified Root Cause (2026-08-08, PR #16721 implementation)

The premise sharpened during implementation, in two places:

  1. "Nothing re-queries" was FALSE — written before checking FleetCockpit#startLiveness: the 15s liveness cadence re-drives roster/activity/brain-health continuously. The smoke's fleetMethods observable shows ~20× fleetRoster/fleetActivity per window lifetime.
  2. The transport ANSWERS. The decisive receipt (brainUp: true + rosterState: 'sample') resolved to: the shell-spawned server reads host .neo-ai-data/fleet/registry.json, which does not exist post-hard-cut, so fleetRoster legitimately answers {rows: []}. The view's designed empty-unselected guard kept the sample seed while recording NO cause, so deriveSpineBanner fell through to the generic "Fleet server offline — start it" line: a false claim carrying shell-harmful advice.

The fix therefore centers on cause retention/retraction truth, not on re-query machinery.

Decision Ledger

  • Part 3 (fleet-ready push): DROPPED — premise falsified (see above); the liveness cadence already satisfies the auto-transition mechanism, and the Reconnect button collapses the worst-case one-cadence wait to "now". No new IPC channel was warranted.
  • Answered-cause lifecycle (the shipped truth model): a producer ANSWER (empty registry / not-wired) retains its cause on the surface; BOTH loss transitions — a thrown call AND an absent bridge/verb — withdraw a never-wired surface's answered cause (the claim must not outlive its producer); wired surfaces keep their stale/live semantics (last-known truth survives absence).
  • Grid badge: static roster · offlinestatic roster — sample proves WHICH data renders, never WHY; the spine banner owns the why.

Contract Ledger (additive consumed surfaces, PR #16721)

Surface Addition Shape Absence/Default Error semantics Consumers Witnesses
resolveUiFleetTransport() outcome (harness/brain.mjs) optional reason String present ONLY for mode: 'foreign-listener'; absent for reuse/spawn n/a — refusal text is harness-authored normalizeTransportFactuiTransportFactbrain-health.transport test/playwright/unit/harness/brain.spec.mjs (foreign-listener outcome)
brain-health IPC response (harness/main.mjs) transport {phase: 'starting'|'settled', mode: String|null, up: Boolean, fleetPort: Number|null, reason: String|null, error: String|null} or null null = no transport story this run (plain UI-only smoke) OR the fact dropped with a failed pull; {phase: 'starting'} while a boot is in flight boot failures ride error (harness-authored text); daemon truth (state/cause) unaffected by the addition FleetCockpit#applyBrainHealthshellTransportderiveSpineBanner cold fallback (text sink only — never HTML) fleetCockpit.spec.mjs (transport-fact-through-health-pull incl. unchanged-fact no-repaint); spineBanner.spec.mjs (8-case fallback matrix)

Acceptance Criteria

  • Offline banner carries a Reconnect control; clicking it with a now-reachable transport transitions the cockpit to live WITHOUT app restart (browser AND shell). [L3-deferred — operator handoff needed → #16694] (control + re-drive shipped and unit-witnessed; the live transition needs a populated reachable transport)
  • Shell foreign-listener case renders the named refusal + correct guidance (never "npm run ai:fleet-server").
  • Shell spawn case: cockpit transitions to live automatically when the transport boot resolves (no click needed). [L3-deferred — operator handoff needed → #16694] (mechanism = the 15s liveness cadence, shipped; the observable transition needs a populated reachable transport)
  • Fail-closed preserved: the button triggers reads through the existing authenticated bridge only — no new transport, no credential surface.
  • Unit coverage for the banner-state routing; smoke stays green.

Out of Scope

  • #16693 (brain-leg attach re-triage) and #16694 (self-supply — merged prerequisite).
  • Stale-transport auto-reaping in product mode (the smoke-mode sweep's product sibling — worth its own ticket if the foreign-listener case recurs; adopting/killing foreign processes is deliberately NOT this leaf).
  • Any polling loop in the browser flow (the button is the browser affordance; polling is a separate decision).

Avoided Traps

  • Auto-adopting a foreign listener — the same-bearer-same-viewer proof is the security floor; the UX names the case instead of weakening the gate.
  • Weakening the capability gate to "retry until reachable" — the gate stays; the RENDERER re-queries, the gate keeps deciding.
  • Shell-only button — the browser flow has the identical stale-offline problem after a late server start; one control serves both.

Related

  • #14560 (cockpit epic — this is a Lane B/E leaf) · #16694 / PR #16696 (the transport self-supply this builds on) · #16693 (sibling) · #15284 (the operable-cold banner this extends) · PR #16721 (the implementing leaf) · D#16720 (the FM-as-client architecture delta this leaf's copy tracks).

Creation-gate record: live latest-8 sweep 2026-08-08T13:40Z — no equivalent; #16694 explicitly excludes UX affordances.

Origin Session ID: 3f80bf5c-66d1-4bc8-bc5a-4dbe482863c9 Retrieval Hint: "cockpit reconnect button offline banner topology-aware fleet-ready auto-recheck"

tobiu referenced in commit bbc5a5f - "feat: cockpit connection truth — reconnect affordance + topology-aware banner (#16699) (#16721) on Aug 8, 2026, 10:30 PM
tobiu closed this issue on Aug 8, 2026, 10:30 PM