Dual-Bus Lane Routing¶
This diagram shows how FCC v1.4.2+ splits the single in-process
:class:EventBus surface into two lane-specific buses and a
:class:LaneRouter that dispatches events to the correct lane
according to an authoritative YAML classification. v1.5.0 carried 85
event types across the paom / ux / both lanes; ADR-006 captures
the decision to adopt the dual-bus architecture to keep orchestration
traffic (workflow / persona / compliance / plugin / ecosystem / object
model) separate from live-view traffic (visualization / demo / tutorial
progress / search + KG + RAG query results).
The flowchart below traces every step in the routing pipeline: producer
emits Event → LaneRouter.classify looks up the lane → publish to
the matching bus(es) → subscribers on each lane receive filtered events
→ transports (WebSocket, SSE, OTel exporter) fan out.
flowchart TB
subgraph Producers["Event producers"]
P1[SimulationEngine]
P2[ActionEngine]
P3[CompliancePipeline]
P4[CollaborationEngine]
P5[MultiUserSession]
P6[SearchIndex / RAG]
P7[BidirectionalSync]
P8[PluginRegistry]
end
Producers --> LR
LR{{"LaneRouter<br/>src/fcc/messaging/lanes.py:132"}}
MAP[("event_lane_map.yaml<br/>src/fcc/data/messaging/<br/>85 classifications")]
LR ---|loads at init| MAP
subgraph PAOMLane["PAOM lane — orchestration / back-office"]
PAOM_Bus[PAOMBus]
PAOM_Types["workflow.*<br/>persona.*<br/>simulation.*<br/>governance.*<br/>action.*<br/>collaboration.*<br/>plugin.*<br/>compliance.*<br/>benchmark.*<br/>ecosystem.*<br/>MCP/A2A (non-UX)<br/>object_model.*<br/>ice_ext.model.*"]
PAOM_Bus --- PAOM_Types
end
subgraph UXLane["UX lane — live-view / visualization"]
UX_Bus[UXBus]
UX_Types["viz.data.updated<br/>viz.frame.rendered<br/>demo.progress.*<br/>tutorial.progress.*<br/>search.query.result<br/>knowledge_graph.query.result<br/>rag.query.progress<br/>rag.query.result"]
UX_Bus --- UX_Types
end
subgraph BothLane["Cross-cutting — both lanes"]
Both_Types["PROTOCOL_BRIDGE_ERROR<br/>COLLABORATION_MATURITY_ASSESSED"]
end
LR -->|classify == 'paom'<br/>publish to paom_bus| PAOM_Bus
LR -->|classify == 'ux'<br/>publish to ux_bus| UX_Bus
LR -->|classify == 'both'| PAOM_Bus
LR -->|classify == 'both'| UX_Bus
LR -.unknown type<br/>RuntimeWarning<br/>fallback PAOM.-> PAOM_Bus
subgraph Subscribers_Paom["PAOM-lane subscribers"]
S1[CompliancePipeline]
S2[EventReplay / SessionRecorder]
S3[OTel span exporter]
S4[DLQ + circuit breaker]
end
subgraph Subscribers_Ux["UX-lane subscribers"]
S5[WebSocket bridge :8765]
S6[SSE stream :8901]
S7[React frontend]
S8[Streamlit live demo]
end
PAOM_Bus --> S1
PAOM_Bus --> S2
PAOM_Bus --> S3
PAOM_Bus --> S4
UX_Bus --> S5
UX_Bus --> S6
UX_Bus --> S7
UX_Bus --> S8
Three operational notes. First, the classification is loaded once at
LaneRouter.__init__ from src/fcc/data/messaging/event_lane_map.yaml;
updating the YAML requires a process restart. Second, unknown event
types fall back to the PAOM lane (orchestration noise) with a
RuntimeWarning — the warning is the signal to add a classification
entry for the new type. Third, the both lane invokes a registered
handler twice (once per bus); callers that must see each event
exactly once use subscribe_paom or subscribe_ux directly instead of
subscribe_both.
Reach for the dual-bus design whenever a new event type is added to
EventType: decide which lane owns it (does the UX need to react live?
or is it orchestration-only?), append a classification entry to the
YAML, and the router picks it up automatically. When the decision is
genuinely both (e.g. a protocol bridge error that compliance must log
AND the UI must surface), use the both lane — it exists for that
exact case.
Stage-by-stage data shapes¶
- Stage 1 — Producer — Any subsystem constructs an
Event(event_type, source, payload, correlation_id)and callsLaneRouter.publish(event). - Stage 2 — Classify —
LaneRouter.classify(event_type)returns"paom"/"ux"/"both"by YAML lookup. - Stage 3 — Publish — Router dispatches to
paom_bus.publish(event),ux_bus.publish(event), or both; delivery counts sum. - Stage 4 — Subscriber match — Each bus delivers to global subscribers, type-specific subscribers, and filtered subscribers in that order (see
src/fcc/messaging/bus.py:134). - Stage 5 — Transport — PAOM lane typically feeds CompliancePipeline / SessionRecorder / OTel exporter / DLQ; UX lane typically feeds WebSocket bridge / SSE stream / React frontend / Streamlit apps.
See also¶
- Source:
src/fcc/messaging/lanes.py:132(LaneRouter),src/fcc/messaging/bus.py:54(EventBusdeprecated single-bus),src/fcc/messaging/events.py(EventTypeenum) - Classification:
src/fcc/data/messaging/event_lane_map.yaml(85 entries) - Related sequence:
../sequence-diagrams/event-bus-pubsub.md - Related class diagram:
six-pillars-overview.md(Pillar D + Pillar B both flow events here) - ADR:
docs/decisions/ADR-006_*.md(dual-bus events) - Universal services:
docs/ecosystem/universal-services.md— UX REST gateway (entry 5) consumes UX lane