Route operational state (Started/Stopped/Suspended) is currently inferred from command ACKs — the server guesses what state a route is in based on the last successful command. This fails when:
- An agent restarts (routes reset to Started, but server doesn't know)
- A route is stopped/suspended outside Cameleer (JMX, Hawtio, programmatic CamelContext)
- The server restarts (all state is lost)
- A new agent joins with routes already in non-default states
## Solution
Two complementary mechanisms, both backward-compatible:
1.**Heartbeat extension** — agent includes current route states in every heartbeat
2.**Route state change events** — agent emits an event immediately when any route changes state
The heartbeat is the steady-state source of truth. Events provide real-time updates between heartbeats.
## 1. Heartbeat Extension
### Current Protocol
```
POST /api/v1/agents/{id}/heartbeat
Authorization: Bearer <token>
(empty body)
→ 200 OK
```
### Extended Protocol
```
POST /api/v1/agents/{id}/heartbeat
Authorization: Bearer <token>
Content-Type: application/json
{
"routeStates": {
"file-processing": "Started",
"timer-heartbeat": "Started",
"error-handling-test": "Suspended",
"try-catch-test": "Stopped"
}
}
```
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `routeStates` | `Map<String, String>` | No | Current state of each Camel route. Keys are route IDs, values are `"Started"`, `"Stopped"`, or `"Suspended"`. |
**Backward compatibility:** The body is optional. Agents that don't send it (older versions) continue working — the server falls back to ACK-based inference for those agents. The `Content-Type` header is only required when a body is present.
### Server Handling
When a heartbeat includes `routeStates`:
1. Update `RouteStateRegistry` with the reported states
2. For routes present in the map, set their state directly (this is authoritative)
3. For routes NOT in the map but previously tracked, mark as `STARTED` (agent didn't report them = default state)
4. Clear any ACK-inferred state for this agent's routes — heartbeat is the source of truth
**Conflict resolution:** If multiple agents for the same application report different states for the same route, use the **most restrictive** state: `STOPPED > SUSPENDED > STARTED`. This handles split-brain scenarios where a command partially succeeded.
RouteState current = getState(applicationId, routeId);
// Most restrictive wins across agents
if (reported.ordinal() > current.ordinal()) {
setState(applicationId, routeId, reported);
} else if (reported == RouteState.STARTED && current != RouteState.STARTED) {
// If this agent says STARTED but another said STOPPED,
// keep STOPPED until ALL agents report STARTED
// (track per-agent state to resolve this properly)
}
}
}
```
**Note on multi-agent conflict:** The simple "most restrictive wins" heuristic works for the common case. A more precise implementation would track per-agent route states and compute the aggregate. This can be deferred — single-agent-per-app deployments cover most early use cases.
## 2. Route State Change Events
### Event Type
Agents emit a `ROUTE_STATE_CHANGED` event via the existing event ingestion endpoint whenever a Camel route changes operational state.
| `details.previousState` | `String` | Yes | State before change: `"Started"`, `"Stopped"`, `"Suspended"` |
| `details.newState` | `String` | Yes | State after change |
| `details.reason` | `String` | No | What triggered the change: `"command"` (Cameleer command), `"external"` (JMX/Hawtio/programmatic), `"startup"` (route starting on agent boot), `"error"` (route stopped due to error) |
| `details.commandId` | `String` | No | If triggered by a Cameleer command, the command ID |
### Server Handling
When `EventIngestionController` receives a `ROUTE_STATE_CHANGED` event:
1. Store the event in the agent events table (existing behavior — all events are stored)
// Also update local state map used for heartbeat reporting
}
}
```
For heartbeat route state reporting, the agent maintains an in-memory map of route states, updated by the same `EventNotifier`. The heartbeat sends this map on each tick.
1. Add `RouteStateEventNotifier` to detect state changes
2. Maintain local `routeStates` map
3. Include `routeStates` in heartbeat payload
4. Emit `ROUTE_STATE_CHANGED` events on state transitions
### Backward Compatibility
- Agents without this extension: heartbeat body is empty, no events emitted. Server falls back to ACK-based inference (current behavior).
- Server without this extension: ignores unknown heartbeat body fields and stores `ROUTE_STATE_CHANGED` events as generic events (no registry update).
- Protocol version stays at `1` — this is an additive extension, not a breaking change.
## Not In Scope
- **Persistence of route state** — intentionally omitted. Route state is runtime state that resets with the application. Persisting it would create stale data after app restarts.
- **Per-agent state tracking** — the registry tracks aggregate state per application+route. Per-agent granularity can be added later if multi-agent split-brain scenarios are common.
- **Route state in SSE push** — the server does not push state changes to the UI via SSE. The UI polls the catalog API (existing 15s refresh interval) which includes route state.