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:
- 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.
- 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.
- 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:
- "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.
- 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 · offline → static 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 |
normalizeTransportFact → uiTransportFact → brain-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#applyBrainHealth → shellTransport → deriveSpineBanner 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
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"
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:
bootUiFleetTransport()resolvesup: true— the renderer never hears about it.The Architectural Reality
apps/agentosfleet views (the fail-closed routing matrices from the W2 arc; the banner is #15284's operable-cold surface).preload.cjsexposes IPC (fleet-request); afleet-readypush (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:
FleetCockpit#startLiveness: the 15s liveness cadence re-drives roster/activity/brain-health continuously. The smoke'sfleetMethodsobservable shows ~20×fleetRoster/fleetActivityper window lifetime.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, sofleetRosterlegitimately answers{rows: []}. The view's designed empty-unselected guard kept the sample seed while recording NO cause, soderiveSpineBannerfell 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
fleet-readypush): 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.static roster · offline→static roster— sample proves WHICH data renders, never WHY; the spine banner owns the why.Contract Ledger (additive consumed surfaces, PR #16721)
resolveUiFleetTransport()outcome (harness/brain.mjs)reasonStringmode: 'foreign-listener'; absent forreuse/spawnnormalizeTransportFact→uiTransportFact→brain-health.transporttest/playwright/unit/harness/brain.spec.mjs(foreign-listener outcome)brain-healthIPC response (harness/main.mjs)transport{phase: 'starting'|'settled', mode: String|null, up: Boolean, fleetPort: Number|null, reason: String|null, error: String|null}ornullnull= no transport story this run (plain UI-only smoke) OR the fact dropped with a failed pull;{phase: 'starting'}while a boot is in flighterror(harness-authored text); daemon truth (state/cause) unaffected by the additionFleetCockpit#applyBrainHealth→shellTransport→deriveSpineBannercold 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
[L3-deferred — operator handoff needed → #16694](control + re-drive shipped and unit-witnessed; the live transition needs a populated reachable transport)[L3-deferred — operator handoff needed → #16694](mechanism = the 15s liveness cadence, shipped; the observable transition needs a populated reachable transport)Out of Scope
Avoided Traps
Related
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"