> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiroleague.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Communication Manager

> The Hiro Server component that routes UnifiedMessages between channels and the application core.

export const DiagramViewer = ({children, title = "Diagram"}) => {
  const dialogRef = useRef(null);
  const viewportRef = useRef(null);
  const contentRef = useRef(null);
  const [isOpen, setIsOpen] = useState(false);
  const [zoom, setZoom] = useState(1);
  const [pan, setPan] = useState({
    x: 0,
    y: 0
  });
  const zoomRef = useRef(1);
  const panRef = useRef({
    x: 0,
    y: 0
  });
  const dragRef = useRef(null);
  const open = () => {
    setZoom(1);
    setPan({
      x: 0,
      y: 0
    });
    setIsOpen(true);
  };
  const close = () => {
    setZoom(1);
    setPan({
      x: 0,
      y: 0
    });
    setIsOpen(false);
  };
  const resetView = () => {
    fitDiagramToViewport();
    setPan({
      x: 0,
      y: 0
    });
  };
  const setView = (nextZoom, nextPan) => {
    zoomRef.current = nextZoom;
    panRef.current = nextPan;
    setZoom(nextZoom);
    setPan(nextPan);
  };
  const MaximizeIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5" />
    </svg>;
  const ResetIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M3 12a9 9 0 1 0 3-6.7L3 8" />
      <path d="M3 3v5h5" />
    </svg>;
  const CloseIcon = () => <svg aria-hidden="true" className="diagram-viewer__icon" viewBox="0 0 24 24">
      <path d="M18 6 6 18M6 6l12 12" />
    </svg>;
  const fitDiagramToViewport = () => {
    const viewport = viewportRef.current;
    const content = contentRef.current;
    const svg = content?.querySelector(".mermaid svg[viewBox]");
    if (!viewport || !svg) return;
    const viewBox = svg.getAttribute("viewBox")?.split(/\s+/).map(Number);
    if (!viewBox || viewBox.length !== 4 || !viewBox[2] || !viewBox[3]) return;
    const [, , width, height] = viewBox;
    svg.style.width = `${width}px`;
    svg.style.maxWidth = "none";
    const rect = viewport.getBoundingClientRect();
    const nextZoom = Math.min(2.2, Math.max(0.75, Number((Math.min((rect.width - 96) / width, (rect.height - 96) / height) * 0.98).toFixed(3))));
    setView(nextZoom, {
      x: 0,
      y: 0
    });
  };
  const handleWheel = event => {
    if (!isOpen) return;
    event.preventDefault();
    event.stopPropagation();
    const viewport = viewportRef.current;
    if (!viewport) return;
    const rect = viewport.getBoundingClientRect();
    const pointerX = event.clientX - rect.left - rect.width / 2;
    const pointerY = event.clientY - rect.top - rect.height / 2;
    const currentZoom = zoomRef.current;
    const currentPan = panRef.current;
    const nextZoom = Math.max(0.25, Math.min(5, Number((currentZoom * (event.deltaY < 0 ? 1.12 : 0.88)).toFixed(3))));
    const ratio = nextZoom / currentZoom;
    setView(nextZoom, {
      x: pointerX - (pointerX - currentPan.x) * ratio,
      y: pointerY - (pointerY - currentPan.y) * ratio
    });
  };
  const onPointerDown = event => {
    if (!isOpen || event.button !== 0) return;
    event.preventDefault();
    dragRef.current = {
      pointerId: event.pointerId,
      startX: event.clientX,
      startY: event.clientY,
      pan: panRef.current
    };
    try {
      event.currentTarget.setPointerCapture?.(event.pointerId);
    } catch {}
  };
  const onPointerMove = event => {
    const drag = dragRef.current;
    if (!isOpen || !drag || drag.pointerId !== event.pointerId) return;
    event.preventDefault();
    const nextPan = {
      x: drag.pan.x + event.clientX - drag.startX,
      y: drag.pan.y + event.clientY - drag.startY
    };
    panRef.current = nextPan;
    setPan(nextPan);
  };
  const endDrag = event => {
    if (dragRef.current?.pointerId === event.pointerId) {
      dragRef.current = null;
    }
  };
  useEffect(() => {
    const dialog = dialogRef.current;
    if (isOpen && dialog && !dialog.open) {
      dialog.showModal();
      requestAnimationFrame(() => {
        requestAnimationFrame(fitDiagramToViewport);
      });
    }
    if (!isOpen && dialog?.open) {
      dialog.close();
    }
  }, [isOpen]);
  useEffect(() => {
    if (!isOpen) return;
    const onKeyDown = event => {
      if (event.key === "Escape") close();
    };
    const onWheel = event => {
      event.preventDefault();
      event.stopPropagation();
      if (viewportRef.current?.contains(event.target)) {
        handleWheel(event);
      }
    };
    const previousOverflow = document.body.style.overflow;
    const previousHtmlOverflow = document.documentElement.style.overflow;
    document.body.style.overflow = "hidden";
    document.documentElement.style.overflow = "hidden";
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("wheel", onWheel, {
      capture: true,
      passive: false
    });
    return () => {
      document.body.style.overflow = previousOverflow;
      document.documentElement.style.overflow = previousHtmlOverflow;
      window.removeEventListener("keydown", onKeyDown);
      window.removeEventListener("wheel", onWheel, {
        capture: true
      });
    };
  }, [isOpen]);
  const toolbar = <div className="diagram-viewer__bar">
      <span className="diagram-viewer__title">{title}</span>
      <div className="diagram-viewer__actions">
        {isOpen && <span className="diagram-viewer__zoom" aria-live="polite">
            {Math.round(zoom * 100)}%
          </span>}
        {isOpen && <button className="diagram-viewer__button" type="button" onClick={resetView} aria-label="Reset diagram">
            <ResetIcon />
          </button>}
        <button className="diagram-viewer__button" type="button" onClick={isOpen ? close : open} aria-label={isOpen ? "Close" : "Maximize"}>
          {isOpen ? <CloseIcon /> : <MaximizeIcon />}
        </button>
      </div>
    </div>;
  const diagram = <div ref={viewportRef} className="diagram-viewer__viewport" onPointerDown={onPointerDown} onPointerMove={onPointerMove} onPointerUp={endDrag} onPointerCancel={endDrag}>
      <div className="diagram-viewer__stage" style={{
    transform: isOpen ? `translate(${pan.x}px, ${pan.y}px)` : undefined
  }}>
        <div className="diagram-viewer__centerer">
          <div ref={contentRef} className="diagram-viewer__content" style={{
    transform: isOpen ? `scale(${zoom})` : "none"
  }}>
            {children}
          </div>
        </div>
      </div>
    </div>;
  return <>
      {!isOpen && <div className="diagram-viewer not-prose">
          {toolbar}
          {diagram}
        </div>}

      <dialog ref={dialogRef} className="diagram-viewer-dialog" onWheel={event => {
    if (event.target === event.currentTarget) {
      event.preventDefault();
      event.stopPropagation();
    }
  }} onCancel={event => {
    event.preventDefault();
    close();
  }} onClose={() => setIsOpen(false)}>
        {isOpen && <div className="diagram-viewer diagram-viewer--open not-prose">
            {toolbar}
            {diagram}
          </div>}
      </dialog>
    </>;
};

Communication Manager is the internal message router for Hiro Server.

Channel Manager owns transport and plugin processes. Agent Manager owns LLM execution. Communication Manager sits between them. It validates inbound `UnifiedMessage` payloads, routes them by `message_type`, enriches user messages before they reach the agent, and sends outbound messages back to the correct channel.

The component is a small runtime facade over focused collaborators. Its job is composition, routing, queue ownership, and lifecycle.

<DiagramViewer title="Communication Manager boundary">
  ```mermaid actions={false} theme={null}
  flowchart TB
      subgraph MessageType["Message Type"]
          direction LR
              MessageHandler["Message Handler"]
              EventHandler["Event Handler"]
              RequestHandler["Request Handler"]
      end
      subgraph MessageF["Message Handler"]
          direction TB
              MessageFlow["Message Flow"]
              AdapterPipeline["Message Adapter Pipeline"]
              PostHooks["Post-adapt Hooks"]
      end
      subgraph CommunicationManager["Communication Manager"]
          direction TB
          InboundPipeline["Inbound Pipeline"]
          MessageType
          MessageF
          OutboundPipeline["Outbound Pipeline"]
      end
      subgraph HiroServer[" "]
          direction TB
              CommunicationManager
              ChannelManager["Channel Manager"]
              AgentManager["Agent Manager"]
              ResourceBroadcaster["Resource Change Broadcaster"]
      end
      %%ChannelManager ~~~ CommunicationManager
      InboundPipeline ~~~ MessageType
      MessageType ~~~ MessageF
      MessageF ~~~ OutboundPipeline
      MessageHandler --> MessageFlow
      InboundPipeline -->|"event"| EventHandler
      InboundPipeline -->|"message"| MessageHandler
      InboundPipeline -->|"request"| RequestHandler
      MessageFlow --> AdapterPipeline
      AdapterPipeline --> PostHooks
      ChannelManager -->|"on_message"| InboundPipeline
      PostHooks -->|"agent-ready message"| AgentManager
      PostHooks -->|"message.transcribed"| OutboundPipeline
      MessageFlow -->|"message.received"| OutboundPipeline
      RequestHandler -->|"response"| OutboundPipeline
      ResourceBroadcaster -->|"resource.changed"| OutboundPipeline
      AgentManager -->|"reply / message.voiced"| OutboundPipeline
      OutboundPipeline -->|"send_to_channel"| ChannelManager1["Back to Channel Manager "]

      style CommunicationManager fill:#EF444455
      style HiroServer fill:transparent, stroke:transparent

  ```
</DiagramViewer>

## Responsibilities

**Inbound validation** - `InboundPipeline.receive()` is the Channel Manager's `on_message` callback target. It validates raw dictionaries into `UnifiedMessage` objects before any downstream component sees them.

**Inbound routing** - The inbound pipeline applies the permission placeholder, then dispatches by `message_type`: `message` goes to `MessageFlow`, `request` goes to `RequestHandler`, `event` goes to `EventHandler`, and unknown types produce a routing-error response.

**Message acknowledgment** - `MessageFlow` immediately emits a `message.received` event for each inbound `message`. This gives the device a delivery signal before adaptation or agent work starts.

**Content adaptation** - `MessageFlow` runs `MessageAdapterPipeline` in a background task. Audio and image adapters enrich content items by writing `metadata.description`; adapter failures write `metadata.adapter_error`.

**Post-adapt side effects** - Post-adapt hooks run after enrichment. They log adapter errors, emit transcript events, persist inbound messages, and finally enqueue the enriched message for Agent Manager.

**Request and event dispatch** - `RequestHandler` handles request/response methods such as `channels.list`, `messages.history`, and `policy.get`. `EventHandler` handles application-level message events carried as `UnifiedMessage` objects.

**Outbound dispatch** - `OutboundPipeline` owns the outbound queue. It drains messages, applies the outbound permission placeholder, and calls the configured `OutboundSink`, currently `ChannelManager.send_to_channel()`.

***

## Main components

### **`CommunicationManager`**

The facade in `runtime/communication_manager.py`. It wires the inbound pipeline, outbound pipeline, message flow, adapter pipeline, request handler, event handler, and post-adapt hook chain.

### **`InboundPipeline`**

The inbound router. It validates raw input, runs inbound permission checks, opens logging scope, and dispatches by `message_type`.

It is callback-driven. There is no inbound worker today; Channel Manager calls `receive()` directly for each `channel.receive` notification.

### **`OutboundPipeline`**

`OutboundPipeline` owns the outbound FIFO queue and its worker. `OutboundSink` is the protocol it depends on.

### **`EnvelopeFactory`**

The central builder for server-originated `UnifiedMessage` envelopes.

It creates:

| Envelope                 | Used for                                        |
| ------------------------ | ----------------------------------------------- |
| `message.received`       | Immediate delivery ack for inbound messages.    |
| `message.transcribed`    | Transcript event for audio messages.            |
| `resource.changed`       | Resource-sync hint to connected devices.        |
| `response`               | Request success or failure response.            |
| `routing_error_response` | Error response for unroutable inbound messages. |

***

## Handlers

### **`1. Message Handler`**

**`MessageFlow`**

The handler for `message_type == "message"`.

It does two things: emit `message.received` immediately, then spawn a detached adaptation task. This keeps channel receive latency low even when audio transcription or image analysis takes time.

**`MessageAdapterPipeline`**

The media enrichment layer. It runs applicable adapters concurrently for a single message.

| Adapter                       | Input                  | Output                                               |
| ----------------------------- | ---------------------- | ---------------------------------------------------- |
| `Audio Transcription Adapter` | `content_type = audio` | Transcript in `item.metadata["description"]`         |
| `Image Understanding Adapter` | `content_type = image` | Visual description in `item.metadata["description"]` |

The original `ContentItem.body` remains unchanged.

**Post-adapt hooks**

Post-adapt hooks are ordered side-effect units that run after the adapter pipeline.

| Hook                  | What it does                                                      |
| --------------------- | ----------------------------------------------------------------- |
| `AdapterErrorLogHook` | Logs per-item adapter failures from `metadata.adapter_error`.     |
| `AudioTranscriptHook` | Emits `message.transcribed` when an audio item has a transcript.  |
| `PersistenceHook`     | Persists the inbound message; failures are logged as non-fatal.   |
| `InboundEnqueueHook`  | Places the enriched message on `inbound_queue` for Agent Manager. |

The order matters. Agent Manager should only see a message after transcript events and persistence attempts have already been handled.

### **`2. RequestHandler`**

The request method registry. It parses the first JSON content item in a request, calls the registered async method, and returns a response `UnifiedMessage`.

The inbound pipeline receives that response and enqueues it through the normal outbound path.

### **`3. EventHandler`**

The dispatcher for application-level message events. These are validated `UnifiedMessage` objects where `message_type == "event"`.

This is separate from `ChannelEventHandler`, which handles raw plugin infrastructure events such as `pairing_request` and `gateway_connected`.

## Events

Communication Manager handles message events, not channel infrastructure events.

| Type           | Transport                              | Format                     | Owner                                          |
| -------------- | -------------------------------------- | -------------------------- | ---------------------------------------------- |
| Channel events | `channel.event` JSON-RPC               | Raw dict                   | `ChannelEventHandler` and `InfraEventHandlers` |
| Message events | `UnifiedMessage(message_type="event")` | Validated `UnifiedMessage` | `EventHandler` and outbound envelope producers |

Active server-originated message events:

| Event                 | Producer                    | Purpose                                                   |
| --------------------- | --------------------------- | --------------------------------------------------------- |
| `message.received`    | `MessageFlow`               | Delivery acknowledgment for the original inbound message. |
| `message.transcribed` | `AudioTranscriptHook`       | Transcript mirror for an audio content item.              |
| `message.voiced`      | `AgentManager`              | Audio mirror for a text reply after TTS synthesis.        |
| `resource.changed`    | `ResourceChangeBroadcaster` | Resource-sync hint for connected devices.                 |

***

## Related components

### **Channel Manager**

Channel Manager is the supplier of UnifiedMessages to Communication Manager through the InboundPipeline. Outbound Pipeline supplies UnifiedMessages back to Channel Manager through the OutboundSink.

### **Agent Manager**

The primary consumer of `inbound_queue`. It is the AI brain of Hiro Server.

### **Resource Change Broadcaster**

An outbound producer that emits `resource.changed` events when workspace resources change. It uses Communication Manager's outbound queue but does not participate in inbound routing.

***

## See also

<CardGroup cols={2}>
  <Card title="Channel Manager" icon="plug" href="/architecture/concepts/channel-manager">
    The transport manager that owns channel plugins and WebSocket JSON-RPC.
  </Card>

  <Card title="Hiro Server components" icon="server" href="/architecture/concepts/hiro-server-components">
    How the main server components relate to each other.
  </Card>

  <Card title="Channel Plugins" icon="plug" href="/architecture/concepts/channel-plugins">
    The plugin-side contract that feeds messages into Channel Manager.
  </Card>
</CardGroup>
