LearnNewsExamplesServices
Frontmatter
id14978
titleFleet cockpit roster never re-polls after a lifecycle intent settles
stateClosed
labels
bugai
assigneesneo-opus-ada
createdAtJul 10, 2026, 8:08 PM
updatedAtJul 10, 2026, 11:43 PM
githubUrlhttps://github.com/neomjs/neo/issues/14978
authorneo-opus-ada
commentsCount0
parentIssue13015
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 10, 2026, 11:43 PM

Fleet cockpit roster never re-polls after a lifecycle intent settles

neo-opus-ada
neo-opus-ada commented on Jul 10, 2026, 8:08 PM

Context

Sub of Epic #13015 (Fleet Manager MVP — define, start, observe). PR #14960 (@neo-opus-vega) landed define + the honest lifecycle round-trip: a card's control fires an intent-only lifecycleIntent, FleetCockpitController.onAgentLifecycleIntent drives the C2 adapter (fleetLifecycleIntentAdapter.handleFleetLifecycleIntent) → the registry bridge → honest pendingAction / controlReason state on the record. That is start working through the UI.

What is missing is observe: after a start succeeds, the card never learns the agent is running.

Reshaped from @neo-opus-vega's /peer-role correction on the define→start→observe seam (my original premise — "the pane has no start control" — was wrong: the control exists in the FleetCockpit AgentCard + the Control panel; only the post-settle observe half is the real gap). Session dialogue on #13015; his handoff: "post-intent roster/runtime refresh + the 'starting' state rendered distinctly … completes define→start→observe honestly."

The Problem

Two observable dishonesties in the shipped cockpit, both verified in code on dev:

  1. Stale off after a successful start. FleetCockpit.loadRoster() is the only thing that maps live runtime state onto the roster records (bridge.fleetRoster()record.set(row) per agentId), and it is called exactly once, at onConstructed (apps/agentos/view/fleet/FleetCockpit.mjs:156). FleetCockpitController.onAgentLifecycleIntent calls handleFleetLifecycleIntent(...) fire-and-forget (FleetCockpitController.mjs:69) and never re-polls. So the sequence is: click start → pendingAction:'start' → adapter settles (pendingAction:null, success) → the card's state is still the pre-start off. The operator starts an agent, the spinner clears, and the card says it is off. Only a page reload reveals the truth. For the §04 PoC bar ("@tobiu starts one agent from the UI") that is the whole point failing silently.

  2. starting collapses into off on the primary glyph. AgentCard.applyRecord renders the state-dot from displayState (running→ok, else→off) and does not consult pendingAction (AgentCard.mjs:239-242). While a start is in-flight the control-status text does show start… (:269), but the state-dot — the at-a-glance status — reads off. The primary glyph disagrees with the secondary text, and there is no distinct starting state.

The Architectural Reality

  • apps/agentos/view/fleet/FleetCockpitController.mjs — the B4÷C2 composition root; owns the wire (cards stay intent-only). onAgentLifecycleIntent (:66) and onStartFleet (:37) call the adapter without consuming its returned {ok, status} result.
  • apps/agentos/view/fleet/fleetLifecycleIntentAdapter.mjs — already returns {accepted, ok, status:'settled'|'rejected'|'timeout'|'unauthorized', ...} (:184). The refresh trigger is a matter of consuming a value the adapter already yields.
  • apps/agentos/view/fleet/FleetCockpit.mjsloadRoster() (:247) is idempotent + fail-closed by construction (reconciles the Store, keeps last-known on a malformed/absent answer); safe to call repeatedly. It is the existing refresh mechanism, invoked at the wrong (single) point.
  • apps/agentos/view/fleet/AgentCard.mjsapplyRecord() (:215) is the one read-side consumer of the record; the state-dot set() is :239.

The fix consumes existing surfaces (the adapter's return value; the record's existing pendingAction); it does not cross into transport and adds no bridge path.

The Fix

  1. FleetCockpitController — consume the adapter result: after handleFleetLifecycleIntent(...) resolves with a non-rejected settle, trigger this.component.loadRoster() so runtime truth re-materializes onto the record. onStartFleet refreshes once after the fan-out settles (not per-card). Fail-closed: a rejected/timeout intent already renders its honest reason; no refresh is forced on it.
  2. AgentCard.applyRecord — when pendingAction is set, render the state-dot as a distinct transitional state (starting for a pending start, stopping for a pending stop) instead of the stale displayState, so the primary glyph agrees with the text and never shows off mid-start.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
FleetCockpitController.onAgentLifecycleIntent / onStartFleet this leaf (behavior add) await the adapter result; on non-rejected settle call loadRoster() (once for fan-out) rejected/timeout → no refresh, honest reason stands method JSDoc controller spec: settle→refresh, reject→no-refresh
AgentCard state-dot vocabulary ADR 0032 honest-states render pendingAction → distinct starting/stopping dot state no pending → existing displayState unchanged applyRecord JSDoc card spec asserts pending→starting, absent→prior state
StateDot state values (verify at build) existing dot vocabulary add starting/stopping visual if absent reuse an existing transitional state if one exists JSDoc dot renders the new state distinctly

Decision Record impact

aligned-with ADR 0032 (the #14445 cockpit render-model / honest-states rules — this makes the state-dot honest during a transition). No amendment; no new ADR needed. Not AiConfig (ADR-0019 gate N/A).

Acceptance Criteria

  • After a card's start intent settles successfully, the cockpit re-polls the roster so the card's runtime state advances from off without a page reload.
  • A rejected / timeout / unauthorized intent does NOT force a roster refresh; its honest reason render is preserved.
  • onStartFleet fan-out triggers exactly one roster refresh after the batch settles, not one per card.
  • While pendingAction is set, the state-dot renders a distinct starting / stopping state (never the stale off).
  • Covered by the existing FleetCockpitLifecycleNL e2e (extended to assert post-settle roster state) + focused unit specs for the controller refresh-trigger and the card vocabulary.

Out of Scope

  • The real spawn behind the bridge (Euclid's #14951) — this leaf wires against the stable FLEET_WIRE_METHODS bridge surface; the end-to-end "a real process boots" proof co-gates there.
  • A runnable Brain under the packaged shell (@neo-opus-vega's #14967) — this leaf works in dev-server mode per #13015's parallelism clause.
  • Polling/interval-based liveness (a continuous heartbeat) — this is event-driven refresh on a settled intent only; a periodic runtime poll, if wanted, is a separate leaf.
  • Any change to Accounts.mjs (the config keeper view) or the C2 adapter's transport contract.

Avoided Traps

  • Optimistic success — rejected: the refresh fires only on a genuine settle; a rejected intent keeps its honest reason. The card never fakes running.
  • Refresh-per-card on fan-out — rejected: N cards → N fleetRoster() polls is wasteful and racey; one refresh after the batch.
  • A second start control (my original wrong premise) — rejected: the control exists; only observe was missing.
  • Continuous polling — rejected as scope creep: event-driven post-settle refresh is the honest minimum for the PoC; a liveness heartbeat is a distinct decision.

Related

Epic #13015 · consumes #14960 (define + lifecycle round-trip, merged) · co-gates real-spawn with #14951 · disjoint from #14967 (Brain under shell) · governed by ADR 0032 render-model.

Live latest-open sweep: checked latest 20 open issues + FM-scope search at 2026-07-10T17:2xZ; no equivalent found (#14964 is config, #14951 is spawn, #14967 is hosting, #14972 is launch-coverage — all distinct).

Origin Session ID: d6f084eb-c9bc-4551-8e2e-2bfed6a927aa

Retrieval Hint: "fleet cockpit loadRoster re-poll after lifecycle intent settle starting state observe verb"

tobiu referenced in commit 6bfb716 - "feat(agentos): re-poll the fleet roster after a settled lifecycle intent + honest 'starting' dot (#14978) (#14979) on Jul 10, 2026, 11:43 PM
tobiu closed this issue on Jul 10, 2026, 11:43 PM