The C3 deliberation spec deferred WebSocket push to a follow-up issue. The implementation looked straightforward — wire lifecycle events (start, complete, fail) to PushBroadcaster topics, add per-round convergence updates. Two topics, four event types, mirror the existing market data push pattern.

The first design question was whether per-round convergence updates needed a cross-repo change. ConversationOrchestrator in casehub-blocks runs the debate loop, and it has no hook for observing state after each round. I filed blocks#125 to add a RoundListener SPI. Seemed clean — add the hook upstream, consume it downstream.

The Wrong Execution Path

The decision review caught it. FsiDeliberationStateObserver — the class that bridges MessageObserver and EventSource for the ChoreographedDriver — is called on every channel message and already holds ConversationState. It’s the natural per-round hook. But more importantly, fsitrading’s deliberation doesn’t use ConversationOrchestrator.converse() at all. It uses Patterns.debate() with ExecutionBackend.choreographed(), which builds an ExecutionModel consumed by a separate driver. DebateBuilder never references ConversationOrchestrator. The SPI I filed would have added a hook to a class that fsitrading’s debate path doesn’t touch.

The observer was already there. CommonGroundAnalyser.analyse() and ConvergenceAnalyser.analyse() are static methods — pure computation over ConversationState, no LLM calls, no I/O. Adding convergence analysis and broadcast to onMessage() gives message-level granularity with no cross-repo dependency. We re-scoped blocks#125 as a general improvement rather than closing it.

The CDI Event Precedent

The second finding was about transport. I’d chosen direct calls — orchestrator injects push service, calls broadcast methods. Simple, testable. The review pointed out that C2’s Market Pulse decisions (D4) explicitly chose CDI events for pipeline-to-arena decoupling. Dismissing CDI events for deliberation-to-push is inconsistent with an architectural choice made one branch earlier on the same project.

The “one consumer” argument didn’t hold either. The spec already names commitment-viz, blocks-timeline, and channel-activity as consumers, and C4 will need deliberation failure events for escalation. CDI events are the established pattern. We switched.

The final design: FsiDeliberationOrchestrator fires CDI domain events (DeliberationStartedEvent, DeliberationCompletedEvent, DeliberationFailedEvent). A dedicated FsiDeliberationPushListener observes them and broadcasts to deliberation:active and deliberation:{channelId}. The observer handles per-round convergence directly — it computes common ground and convergence on every message and broadcasts a ConvergenceUpdate payload. All type-discriminated with a type field for client dispatch.

The review also caught the topic naming convention — market data uses colons (market:ticks:AAPL), the spec used slashes (deliberation/{channelId}). Colons for consistency.

The interesting thing about this branch isn’t the implementation — it’s four commits of straightforward wiring. The interesting thing is that the design review found a dependency that would have been wrong, and an inconsistency with a decision made a week earlier. Both would have shipped unnoticed. The cross-repo issue would have sat open, someone would eventually have implemented the SPI, and it wouldn’t have helped.


<
Previous Post
The Review That Paid for Itself
>
Next Post
Three Ways a Quarkus Extension Can Silently Do Nothing