This note documents ownership boundaries in the runtime stack. The intent is to keep transport state, message handling, controller state, and rendering separate so future changes do not reintroduce connection-state regressions.
RC.ROS owns ROS transport lifecycle:
- open/close the ROS bridge connection
- offline mode gating
- connect/disconnect/trying/stopping flags
- initial namespace setup for UI panels
RC.ROS should be the only layer that decides whether the client has an active ROS transport session.
It should not:
- infer behavior-engine execution state from individual runtime messages
- own mirror/onboard/launcher heartbeat interpretation
- directly decide runtime page modes beyond transport connect/disconnect setup
RC.PubSub owns runtime message ingress and command egress:
- subscribe to onboard, mirror, launcher, and state-map topics
- publish runtime commands
- track heartbeat/status/state-map timing
- translate raw messages into higher-level controller signals
RC.PubSub should:
- update execution status based on message content
- detect stale or inconsistent runtime data
- feed status and feedback into
RC.ControllerandUI.RuntimeControl
RC.PubSub should not:
- own ROS transport connection state
- replace
RC.Controlleras the source of truth for UI state transitions
In particular:
- heartbeats confirm runtime liveness
- heartbeats do not establish transport connectivity on their own
- behavior status messages such as
READY,STARTED,FINISHED, andFAILEDmust be translated into controller state transitions explicitly
RC.Controller is the canonical runtime UI state machine.
It owns transitions between states such as:
- offline
- no behavior
- configuration
- starting
- active
- locked
- changed
- external
It is the layer that decides what the runtime UI should display.
RC.Controller should:
- accept explicit signals from transport or pub/sub layers
- own
current_state_pathandlocked_state_path - decide whether the UI is connected, running, locked, external, or configurable
It should not:
- manage ROS transport directly
- parse raw topic messages
UI.RuntimeControl owns rendering and operator interaction for runtime supervision.
It should:
- render the current controller/runtime state
- show current state, lock status, outcome requests, and feedback
- gather current parameter/autonomy/lock selections from the DOM
It should not:
- own transport connection state
- interpret raw heartbeat/status/state-map messages directly
When changing runtime code, use these rules:
- Transport lifecycle belongs in
RC.ROS. - Message interpretation belongs in
RC.PubSub. - UI/runtime mode transitions belong in
RC.Controller. - Rendering and operator interaction belong in
UI.RuntimeControl.
A correct READY path looks like this:
RC.PubSubreceives onboardBEStatus.READY.RC.PubSubsignals the controller that the runtime is connected and no behavior is active.RC.Controllertransitions to the appropriate connected/configuration state.UI.RuntimeControlrenders the corresponding runtime view.
What should not happen:
- relying on heartbeat reception alone to move the shell from disconnected to connected
- leaving
READYas a feedback-only message without updating controller state
The most subtle runtime regressions in this codebase have come from mixing these responsibilities:
- transport looked connected, but controller stayed disconnected
- heartbeats were present, but runtime UI still showed
No Connection - execution feedback arrived, but the shell state machine was not advanced
Keeping these boundaries explicit makes those failures much easier to prevent and debug.
The server readiness endpoint is intentionally read-only:
GET /api/v1/readyreports that the WebUI server is serving requests.- The browser UI then posts
POST /api/v1/ui_connectedto mark that an operator session is active and shutdown should require confirmation. - When the UI is allowed to exit, it posts
POST /api/v1/confirm_shutdown. webui_clientlistens on/ws/check_shutdownso the server can deliver the final shutdown decision back to the desktop window.
When token auth is enabled, the lifecycle mutation endpoints and shutdown websocket require the same API token as other protected control paths.
The HTTP API uses a normalized response envelope:
{
"success": true,
"data": {},
"error": null,
"status": 200
}Use the following rules when adding or updating endpoints:
-
success=falsemeans request-level failure. This includes auth failures, validation failures, malformed requests, transport failures, and command execution failures. -
success=truemeans the request contract succeeded. Domain-specific outcomes belong insidedata. -
Command-style mutation endpoints must return
data.ok. Examples: file editor open, ROS create/close/publish operations. -
Partial success belongs inside
data, not the envelope. Example: code generation can succeed overall while a secondary source-save step fails and reports that failure in dedicateddatafields. -
Aggregation endpoints may be best-effort, but they must stay explicit. If partial results are returned, include structured per-item failures in
datarather than relying on truthiness or omitted fields. -
Frontend code must never use raw truthiness for API responses. Use the helpers in
app/api.jsand stop normal processing immediately whensuccess=false.