Files
cameleer-server/.planning/phases/03-agent-registry-sse-push/03-02-PLAN.md
hsiegeln cb3ebfea7c
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 18s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
chore: rename cameleer3 to cameleer
Rename Java packages from com.cameleer3 to com.cameleer, module
directories from cameleer3-* to cameleer-*, and all references
throughout workflows, Dockerfiles, docs, migrations, and pom.xml.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 15:28:42 +02:00

15 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
03-agent-registry-sse-push 02 execute 2
03-01
cameleer-server-app/src/main/java/com/cameleer/server/app/agent/SseConnectionManager.java
cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentSseController.java
cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentCommandController.java
cameleer-server-app/src/main/java/com/cameleer/server/app/config/WebConfig.java
cameleer-server-app/src/test/java/com/cameleer/server/app/controller/AgentSseControllerIT.java
cameleer-server-app/src/test/java/com/cameleer/server/app/controller/AgentCommandControllerIT.java
true
AGNT-04
AGNT-05
AGNT-06
AGNT-07
truths artifacts key_links
Registered agent can open SSE stream at GET /api/v1/agents/{id}/events and receive events
Server pushes config-update events to a specific agent's SSE stream via POST /api/v1/agents/{id}/commands
Server pushes deep-trace commands to a specific agent's SSE stream with correlationId in payload
Server pushes replay commands to a specific agent's SSE stream
Server can target commands to all agents in a group via POST /api/v1/agents/groups/{group}/commands
Server can broadcast commands to all live agents via POST /api/v1/agents/commands
SSE stream receives ping keepalive comments every 15 seconds
SSE events include event ID for Last-Event-ID reconnection support (no replay of missed events)
Agent can acknowledge command receipt via POST /api/v1/agents/{id}/commands/{commandId}/ack
path provides
cameleer-server-app/src/main/java/com/cameleer/server/app/agent/SseConnectionManager.java Per-agent SseEmitter management, event sending, ping keepalive
path provides
cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentSseController.java GET /{id}/events SSE endpoint
path provides
cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentCommandController.java POST command endpoints (single, group, broadcast) + ack endpoint
from to via pattern
AgentCommandController SseConnectionManager sendEvent for command delivery connectionManager.sendEvent
from to via pattern
AgentCommandController AgentRegistryService addCommand + findByState/findByGroup registryService.addCommand
from to via pattern
SseConnectionManager AgentEventListener implements interface, receives command notifications implements AgentEventListener
from to via pattern
AgentSseController SseConnectionManager connect() returns SseEmitter connectionManager.connect
Build SSE connection management and command push infrastructure for real-time agent communication.

Purpose: The server needs to push config-update, deep-trace, and replay commands to connected agents in real time via Server-Sent Events. This completes the bidirectional communication channel (agents POST data to server, server pushes commands via SSE). Output: SseConnectionManager, SSE endpoint, command controller (single/group/broadcast targeting), command acknowledgement, ping keepalive, Last-Event-ID support, integration tests.

<execution_context> @C:/Users/Hendrik/.claude/get-shit-done/workflows/execute-plan.md @C:/Users/Hendrik/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/03-agent-registry-sse-push/03-CONTEXT.md @.planning/phases/03-agent-registry-sse-push/03-RESEARCH.md @.planning/phases/03-agent-registry-sse-push/03-01-SUMMARY.md

@cameleer-server-app/src/main/java/com/cameleer/server/app/config/WebConfig.java @cameleer-server-app/src/main/resources/application.yml @cameleer-server-app/src/test/java/com/cameleer/server/app/AbstractClickHouseIT.java

From cameleer-server-core/.../agent/AgentInfo.java:

// Record or class with fields:
// id, name, group, version, routeIds, capabilities, state, registeredAt, lastHeartbeat, staleTransitionTime
// Methods: withState(), withLastHeartbeat(), etc.

From cameleer-server-core/.../agent/AgentState.java:

public enum AgentState { LIVE, STALE, DEAD }

From cameleer-server-core/.../agent/CommandType.java:

public enum CommandType { CONFIG_UPDATE, DEEP_TRACE, REPLAY }

From cameleer-server-core/.../agent/CommandStatus.java:

public enum CommandStatus { PENDING, DELIVERED, ACKNOWLEDGED, EXPIRED }

From cameleer-server-core/.../agent/AgentCommand.java:

// Record: id (UUID string), type (CommandType), payload (String JSON), targetAgentId, createdAt, status
// Method: withStatus()

From cameleer-server-core/.../agent/AgentEventListener.java:

public interface AgentEventListener {
    void onCommandReady(String agentId, AgentCommand command);
}

From cameleer-server-core/.../agent/AgentRegistryService.java:

// Key methods:
// register(id, name, group, version, routeIds, capabilities) -> AgentInfo
// heartbeat(id) -> boolean
// findById(id) -> AgentInfo
// findAll() -> List<AgentInfo>
// findByState(state) -> List<AgentInfo>
// addCommand(agentId, type, payload) -> AgentCommand
// acknowledgeCommand(agentId, commandId) -> boolean
// markDelivered(agentId, commandId) -> void
// setEventListener(listener) -> void

From cameleer-server-app/.../config/AgentRegistryConfig.java:

// @ConfigurationProperties(prefix = "agent-registry")
// getPingIntervalMs(), getCommandExpiryMs(), etc.
Task 1: SseConnectionManager, SSE controller, and command controller cameleer-server-app/src/main/java/com/cameleer/server/app/agent/SseConnectionManager.java, cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentSseController.java, cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentCommandController.java, cameleer-server-app/src/main/java/com/cameleer/server/app/config/AgentRegistryBeanConfig.java, cameleer-server-app/src/main/java/com/cameleer/server/app/config/WebConfig.java Build the SSE infrastructure and command delivery system:
1. **SseConnectionManager** (@Component, implements AgentEventListener):
   - ConcurrentHashMap<String, SseEmitter> emitters for per-agent connections
   - Inject AgentRegistryConfig for ping interval, inject AgentRegistryService (call setEventListener(this) in @PostConstruct)
   - `connect(String agentId)`: Create SseEmitter(Long.MAX_VALUE). Register onCompletion/onTimeout/onError callbacks that remove the emitter ONLY if the current map value is the same instance (reference equality via == check to avoid Pitfall 3 from research). Replace existing emitter with put(), complete() old one if exists. Return new emitter.
   - `sendEvent(String agentId, String eventId, String eventType, Object data)`: Get emitter from map, send SseEmitter.event().id(eventId).name(eventType).data(data, MediaType.APPLICATION_JSON). Catch IOException, remove emitter, return false. Return true on success.
   - `sendPingToAll()`: Iterate emitters, send comment("ping") to each. Remove on IOException.
   - `isConnected(String agentId)`: Check if emitter exists in map.
   - `onCommandReady(String agentId, AgentCommand command)`: Attempt sendEvent with command.id() as eventId, command.type().name().toLowerCase().replace('_', '-') as event name (config-update, deep-trace, replay), command.payload() as data. If successful, call registryService.markDelivered(agentId, command.id()). If agent not connected, command stays PENDING (caller can re-send or it expires).
   - @Scheduled(fixedDelayString = "${agent-registry.ping-interval-ms:15000}") pingAll(): calls sendPingToAll()

2. **Update AgentRegistryBeanConfig**: After creating AgentRegistryService bean, the SseConnectionManager (auto-scanned as @Component) will call setEventListener in @PostConstruct. No change needed in bean config if SseConnectionManager handles it. BUT -- to avoid circular dependency, SseConnectionManager should inject AgentRegistryService and call setEventListener(this) in @PostConstruct.

3. **AgentSseController** (@RestController, @RequestMapping("/api/v1/agents")):
   - Inject SseConnectionManager, AgentRegistryService
   - `GET /{id}/events` (produces TEXT_EVENT_STREAM_VALUE): Check agent exists via registryService.findById(id). If null, return 404 (throw ResponseStatusException). Read Last-Event-ID header (optional) -- log it at debug level but do NOT replay missed events (per locked decision). Call connectionManager.connect(id), return the SseEmitter.
   - Add @Tag(name = "Agent SSE") and @Operation annotations.

4. **AgentCommandController** (@RestController, @RequestMapping("/api/v1/agents")):
   - Inject AgentRegistryService, SseConnectionManager, ObjectMapper
   - `POST /{id}/commands`: Accept raw String body. Parse JSON: { "type": "config-update|deep-trace|replay", "payload": {...} }. Map type string to CommandType enum (config-update -> CONFIG_UPDATE, deep-trace -> DEEP_TRACE, replay -> REPLAY). Call registryService.addCommand(id, type, payloadJsonString). The AgentEventListener.onCommandReady in SseConnectionManager handles delivery. Return 202 with { commandId, status: "PENDING" or "DELIVERED" depending on whether agent is connected }.
   - `POST /groups/{group}/commands`: Same body parsing. Find all LIVE agents in group via registryService.findAll() filtered by group. For each, call registryService.addCommand(). Return 202 with { commandIds: [...], targetCount: N }.
   - `POST /commands`: Broadcast to all LIVE agents. Same pattern as group but uses registryService.findByState(LIVE). Return 202 with count.
   - `POST /{id}/commands/{commandId}/ack`: Call registryService.acknowledgeCommand(id, commandId). Return 200 if true, 404 if false.
   - Add @Tag(name = "Agent Commands") and @Operation annotations.

5. **Update WebConfig**: The SSE endpoint GET /api/v1/agents/{id}/events is already covered by the interceptor pattern "/api/v1/agents/**". Agents send the protocol version header on all requests (per research recommendation), so no exclusion needed. However, if the SSE GET causes issues because browsers/clients may not easily add custom headers to EventSource, add the SSE events path to excludePathPatterns: `/api/v1/agents/*/events`. This is a practical consideration -- add the exclusion to be safe.
mvn compile -pl cameleer-server-core,cameleer-server-app SseConnectionManager, AgentSseController, and AgentCommandController compile. SSE endpoint returns SseEmitter. Command endpoints accept type/payload and deliver via SSE. Ping keepalive scheduled. WebConfig updated if needed. Task 2: Integration tests for SSE, commands, and full flow cameleer-server-app/src/test/java/com/cameleer/server/app/controller/AgentSseControllerIT.java, cameleer-server-app/src/test/java/com/cameleer/server/app/controller/AgentCommandControllerIT.java Write integration tests covering SSE connection, command delivery, ping, and acknowledgement:
**SSE Test Strategy** (from RESEARCH.md): Testing SSE with TestRestTemplate is non-trivial. Use one of these approaches:
- Option A (preferred): Use raw HttpURLConnection or java.net.http.HttpClient to open the SSE stream in a separate thread, read lines, and assert event format.
- Option B: Use Spring WebClient (from spring-boot-starter-webflux test dependency) -- BUT do not add webflux as a main dependency, only as test scope if needed.
- Option C: Test at the service layer by calling SseConnectionManager.connect() directly, then sendEvent(), and reading from the SseEmitter via a custom handler.

Recommend Option A (HttpClient) for true end-to-end testing without adding dependencies.

1. **AgentSseControllerIT** (extends AbstractClickHouseIT):
   - Test SSE connect for registered agent: Register agent, open GET /{id}/events with Accept: text/event-stream. Assert 200 and content-type is text/event-stream.
   - Test SSE connect for unknown agent: GET /unknown-id/events, assert 404.
   - Test config-update delivery: Register agent, open SSE stream (background thread), POST /{id}/commands with {"type":"config-update","payload":{"key":"value"}}. Use Awaitility to assert SSE stream received event with name "config-update" and correct data.
   - Test deep-trace delivery: Same pattern with {"type":"deep-trace","payload":{"correlationId":"test-123"}}.
   - Test replay delivery: Same pattern with {"type":"replay","payload":{"exchangeId":"ex-456"}}.
   - Test ping keepalive: Open SSE stream, wait for ping comment (may need to set ping interval low in test config or use Awaitility with timeout). Assert ":ping" comment received.
   - Test Last-Event-ID header: Open SSE with Last-Event-ID header set. Assert connection succeeds (no replay, just acknowledges).
   - All POST requests include X-Cameleer-Protocol-Version:1 header. SSE GET may need the header excluded in WebConfig (test will reveal if this is an issue).
   - Use Awaitility with ignoreExceptions() for async assertions (established pattern).

2. **AgentCommandControllerIT** (extends AbstractClickHouseIT):
   - Test single agent command: Register agent, POST /{id}/commands, assert 202 with commandId.
   - Test group command: Register 2 agents in same group, POST /groups/{group}/commands, assert 202 with targetCount=2.
   - Test broadcast command: Register 3 agents, POST /commands, assert 202 with count of LIVE agents.
   - Test command ack: Send command, POST /{id}/commands/{commandId}/ack, assert 200.
   - Test ack unknown command: POST /{id}/commands/unknown-id/ack, assert 404.
   - Test command to unregistered agent: POST /nonexistent/commands, assert 404.

**Test configuration**: If ping interval needs to be shorter for tests, add to test application.yml or use @TestPropertySource with agent-registry.ping-interval-ms=1000.
mvn test -pl cameleer-server-core,cameleer-server-app -Dtest="Agent*" All SSE integration tests pass: connect/disconnect, config-update/deep-trace/replay delivery via SSE, ping keepalive received, Last-Event-ID accepted, command targeting (single/group/broadcast), command acknowledgement. mvn clean verify passes with all existing tests still green. mvn clean verify -- full suite green (all Phase 1, 2, and 3 tests pass)

<success_criteria>

  • SSE endpoint returns working event stream for registered agents
  • config-update, deep-trace, and replay commands delivered via SSE in real time
  • Group and broadcast targeting works correctly
  • Ping keepalive sent every 15 seconds
  • Last-Event-ID header accepted (no replay, per decision)
  • Command acknowledgement endpoint works
  • All integration tests pass
  • Full mvn clean verify passes </success_criteria>
After completion, create `.planning/phases/03-agent-registry-sse-push/03-02-SUMMARY.md`