---
title: |-
  Playbook *UC3*
  Mac Peers Channels
description: "Verificación: dos sesiones Claude Code en Mac con proxy + channels + a2a MCP vía broker público. Peer discovery + mensajería realtime sin spawn."
kicker: Playbook · UC3 · Mac Peers Channels
subtitle: |-
  Dos sesiones independientes de Claude Code en el mismo Mac, ambas lanzadas con
  `cl minimax --proxy --channels`. El objetivo es verificar que se ven mutuamente
  (ListPeers), que pueden enviarse mensajes (SendPeerMessage) y que los mensajes
  llegan **en tiempo real** vía channel-push del broker, sin polling del inbox.
strip:
  - { k: PLAYBOOK, v: UC3-CHANNELS }
  - { k: REV, v: "1.0" }
  - { k: FECHA, v: 21 JUN 2026 }
  - { k: DEPENDE, v: P0 + UC1 }
meta:
  - { k: Esfuerzo, v: 15 min }
  - { k: Depende de, v: "P0 (prerequisites) + UC1 (proxy)" }
  - { k: Bloquea, v: "UC4 (spawn) · UC5 (workspace E2E)" }
  - { k: Criticidad, v: "[[bad:ALTA]] — si esto falla, el ecosistema a2a está roto" }
stamps:
  - { text: Broker prod vivo, tone: ok }
  - { text: a2a MCP configurado en Mac, tone: ok }
  - { text: Sin verificación runtime, tone: warn }
toc_legend:
  - { text: verificado, tone: ok }
  - { text: fallo esperable, tone: warn }
  - { text: crítico, tone: bad }
footer:
  left: |-
    **UC3-CHANNELS · REV 1.0**
    Playbooks de verificación E2E para mks-agentics
  right: |-
    **2 PEERS** Mac · **CHANNELS** broker público · **REALTIME**
---

## Objetivo {#objetivo}

Dos sesiones Claude Code en Mac, ambas con `cl minimax --proxy --channels`, deben poder: (1) verse mutuamente vía ListPeers, (2) enviarse mensajes vía SendPeerMessage, (3) recibir los mensajes en **tiempo real** vía channel-push (`<channel>` blocks inline), sin necesidad de polling del inbox. Si esto no funciona, el ecosistema a2a está roto. {.lead}

```dossier:diagram
engine: d2
source: |
  direction: right

  # ── Dos sesiones Claude en Mac ──
  sessionA: "Sesión A\ncl minimax\n--proxy --channels" {
    style: {
      fill: "#1a1a2e"
      stroke: "#e94560"
    }
    mcpA: "a2a-mcp\n(stdio MCP)" {
      style: {
        fill: "#16213e"
        stroke: "#e94560"
      }
    }
  }

  sessionB: "Sesión B\ncl minimax\n--proxy --channels" {
    style: {
      fill: "#1a1a2e"
      stroke: "#533483"
    }
    mcpB: "a2a-mcp\n(stdio MCP)" {
      style: {
        fill: "#16213e"
        stroke: "#533483"
      }
    }
  }

  broker: "Broker público\nbroker.gateway.mks2508.systems" {
    style: {
      fill: "#0f3460"
      stroke: "#e94560"
    }
  }

  # ── Conexiones ──
  mcpA -> broker: "REST: ListPeers\nREST: SendPeerMessage" {
    style.stroke-dash: 3
  }
  mcpB -> broker: "REST: ListPeers\nREST: SendPeerMessage" {
    style.stroke-dash: 3
  }

  broker -> sessionA: "CHANNEL PUSH:\n<channel from_id=\"B\">\nmessage</channel>" {
    style.stroke: "#e94560"
    style.stroke-dash: 3
  }
  broker -> sessionB: "CHANNEL PUSH:\n<channel from_id=\"A\">\nmessage</channel>" {
    style.stroke: "#533483"
    style.stroke-dash: 3
  }

  # ── Flujo ──
  sessionA -> broker: "SendPeerMessage to B\n'hola desde A'" {
    style.stroke: "#e94560"
  }
  broker -> sessionB: "REALTIME PUSH\nsin polling" {
    style.stroke: "#533483"
  }
```

## Arquitectura del *channel-push* {#arquitectura}

```dossier:arch
no: "1"
title: Channel-push — Mecanismo realtime
sub: WebSocket al broker → server-push → <channel> block inline en Claude Code
diagram:
  top: Dos mecanismos complementarios
  bus: |
    MCP tools (REST) → SendPeerMessage, ListPeers, CheckInbox
    Channel-push (WS) → /ws/broker/peer/:id → push server-initiated
  nodes:
    - name: MCP tools
      sub: 13 tools vía REST al broker
      items:
        - SendPeerMessage → POST /api/peers/:id/message
        - ListPeers → GET /api/peers
        - CheckInbox → POST /api/peers/:id/inbox/read
    - name: Channel-push WS
      sub: Conexión persistente al broker
      items:
        - A2A_CHANNEL_PUSH=1 activa el WS
        - Recibe mensajes como notifications/claude/channel
        - Ack selectivo tras renderizado
        - Reconnect con backoff exponencial
        - Seen-cache (500 IDs) anti-duplicados
  link: https://broker.gateway.mks2508.systems
  workloads:
    - Mensajería instantánea peer-to-peer
    - Sin polling del inbox
  stats:
    - { v: "2", k: conexiones WS, tone: ok }
    - { v: "13", k: tools MCP, tone: ok }
    - { v: OIDC, k: auth, tone: ok }
  pros:
    - Realtime sin polling
    - <channel> block integrado en Claude Code UI
    - Mismo broker para tools y push
  cons:
    - Requiere dangerouslyLoadDevelopmentChannels
    - Sin HA (single broker)
```

```dossier:note
tone: accent
tag: Requisitos para channel-push
body: |
  Para que el channel-push funcione, la sesión Claude Code necesita **ambas cosas**:

  1. **a2a MCP configurado** — `mcpServers.agent2agent` con `GATEWAY_BASE_URL` apuntando al broker
     y `A2A_CHANNEL_PUSH=1`
  2. **`dangerouslyLoadDevelopmentChannels: ["server:agent2agent"]`** — en el `settings.json`
     del provider dir que se está usando

  Ambas cosas las mergea `claudio --channels` automáticamente desde `~/.claude/settings.json`.
  **NO editar settings.json manualmente** — siempre usar `claudio` para configurar modos.
```

## F1 — *Preparación* {#f1-prep}

```dossier:phases
items:
  - dur: "3 min"
    title: Verificar configuración antes de arrancar
    checks:
      - text: "`claudio minimax --env --channels` → emite exports sin errores"
        sub: Verificar que claudio puede mergear a2a MCP + channels para MiniMax
      - text: "`cat ~/.claude-minimax/settings.json | python3 -c \"import json,sys; d=json.load(sys.stdin); print('a2a' in d.get('mcpServers',{}))\"` → True"
        sub: a2a MCP está en el settings.json del provider dir
      - text: "`cat ~/.claude-minimax/settings.json | python3 -c \"import json,sys; d=json.load(sys.stdin); print(d.get('dangerouslyLoadDevelopmentChannels'))\"` → ['server:agent2agent']"
        sub: Channels flag está en el settings.json
      - text: "Broker reachable: `curl -s https://broker.gateway.mks2508.systems/healthz` → 200"
        sub: El broker público está vivo
      - text: "OIDC token válido: `source .env.brk-extract.local && curl -s https://broker.gateway.mks2508.systems/api/peers -H \"Authorization: Bearer $TOKEN\"` → 200"
        sub: Autenticación OIDC funciona
```

## F2 — *Arrancar* sesión A {#f2-sessionA}

```dossier:phases
items:
  - dur: "3 min"
    title: Primera sesión Claude Code con channels
    checks:
      - text: "En terminal 1: ejecutar `cl minimax --proxy --channels`"
        sub: Arranca Claude Code con MiniMax vía proxy + channels activos
      - text: "Dentro de Claude Code A, ejecutar `ListPeers`"
        sub: Debe mostrar la lista de peers (al menos ella misma)
      - text: "Ejecutar `GetPeer` con el peerId de la sesión A"
        sub: Debe mostrar la card del peer A con status 'running'
```

### Sesión A — comandos de verificación

```dossier:term
title: Terminal 1 — Sesión A
content: |
  # Arrancar sesión A
  $ cl minimax --proxy --channels

  # Dentro de Claude Code A:
  > ListPeers
  → [
  →   {
  →     "id": "a1b2c3d4-...",
  →     "kind": "operator",
  →     "name": "Claude Code — A",
  →     "status": "running",
  →     ...
  →   }
  → ]

  > GetPeer a1b2c3d4-...
  → (card completa del peer A)

  # Guardar el peerId de A para usarlo desde B
  > (anotar: A_ID = "a1b2c3d4-...")
```

## F3 — *Arrancar* sesión B y peer discovery {#f3-sessionB}

```dossier:phases
items:
  - dur: "3 min"
    title: Segunda sesión — debe ver a la primera
    checks:
      - text: "En terminal 2: ejecutar `cl minimax --proxy --channels` (segunda instancia)"
        sub: Segunda sesión Claude Code independiente
      - text: "Dentro de Claude Code B, ejecutar `ListPeers`"
        sub: "Debe mostrar **DOS** peers: A y B"
      - text: "El peer A aparece con status 'running' en la lista de B"
        sub: La sesión B ve a la sesión A
      - text: "Ejecutar `SendPeerMessage` de B → A con 'hola desde B'"
        sub: Mensaje enviado via REST al broker → entregado a A via channel-push
```

### Sesión B — comandos de verificación

```dossier:term
title: Terminal 2 — Sesión B
content: |
  # Arrancar sesión B en otra terminal
  $ cl minimax --proxy --channels

  # Dentro de Claude Code B:
  > ListPeers
  → [
  →   {"id": "a1b2c3d4-...", "kind": "operator", "name": "Claude Code — A", "status": "running"},
  →   {"id": "e5f6g7h8-...", "kind": "operator", "name": "Claude Code — B", "status": "running"}
  → ]

  # Enviar mensaje a la sesión A:
  > SendPeerMessage to=a1b2c3d4-... content="hola desde B"
  → {"delivered": true, "messageId": "msg_..."}
```

## F4 — Verificar *realtime* channel-push {#f4-realtime}

```dossier:phases
items:
  - dur: "3 min"
    title: El mensaje debe aparecer en tiempo real
    checks:
      - text: "En terminal 1 (sesión A), aparece un bloque `<channel>` inline con el mensaje de B"
        sub: "El bloque se ve como: `<channel source='agent2agent' from_id='e5f6g7h8-...' kind='request' priority='normal'>hola desde B</channel>`"
      - text: "La sesión A puede responder con `SendPeerMessage to=e5f6g7h8-... content='respuesta desde A'`"
        sub: Respuesta de vuelta a B
      - text: "En terminal 2 (sesión B), aparece el bloque `<channel>` con la respuesta de A"
        sub: Bidireccional confirmado
      - text: "NO se usó `CheckInbox` ni `InboxCount` en ningún momento"
        sub: Los mensajes llegaron por channel-push, no por polling
```

### Verificación del channel block inline

```dossier:note
tone: accent
tag: Representación del channel block
body: |
  Cuando un peer recibe un mensaje vía channel-push, Claude Code muestra un bloque
  inline como este — **no** un tool result, **no** un mensaje del sistema. Es un
  `<channel>` block renderizado como notificación inline, similar a un @mention.

  ```
  <channel source="agent2agent" from_id="e5f6g7h8-..."
           kind="request" priority="normal">
  hola desde B
  </channel>
  ```
```

```dossier:term
title: Qué ver en la terminal de la sesión A
content: |
  # Después de que B envía "hola desde B", en la sesión A aparece:

  <channel source="agent2agent" from_id="e5f6g7h8-..."
           kind="request" priority="normal">
  hola desde B
  </channel>

  # La sesión A puede responder directamente:
  > SendPeerMessage to=e5f6g7h8-... content="hola desde A, te he recibido!"
  → {"delivered": true}

  # Y en la sesión B aparece en tiempo real:
  <channel source="agent2agent" from_id="a1b2c3d4-..."
           kind="request" priority="normal">
  hola desde A, te he recibido!
  </channel>
```

```dossier:note
tone: warn
tag: Si NO aparece el channel block inline
body: |
  Posibles causas:
  1. **`dangerouslyLoadDevelopmentChannels` no está en settings.json** — verificar con
     `python3 -c "import json; d=json.load(open('$CLAUDE_CONFIG_DIR/settings.json')); print(d.get('dangerouslyLoadDevelopmentChannels'))"`
  2. **`tengu_harbor` no está pineado a `true`** — GrowthBook puede haberlo sobreescrito.
     Verificar en `~/.claude-minimax/.claude.json` el campo `cachedGrowthBookFeatures.tengu_harbor`
  3. **a2a-mcp no tiene `A2A_CHANNEL_PUSH=1`** — verificar en el `.claude.json` de la sesión
  4. **El broker no tiene el peer registrado** — verificar con `curl /api/peers`
  5. **El WebSocket no se pudo establecer** — revisar logs del a2a-mcp en stderr

  Si todo lo anterior está bien pero aún no llega, el problema está en el channel-push
  WS o en la integración con Claude Code. Revisar `[a2a-mcp]` logs.
```

## F5 — *Diagnóstico* si falla {#f5-diagnostico}

```dossier:phases
items:
  - dur: "5 min"
    title: Diagnóstico de fallos channel-push
    checks:
      - text: "`curl -s https://broker.gateway.mks2508.systems/api/peers -H 'Authorization: Bearer $JWT' | jq 'length'` → ≥2"
        sub: Ambos peers registrados en el broker
      - text: "`a2a inbox <peerId>` → el mensaje está en el inbox (fallback polling)"
        sub: Si el mensaje está en el inbox pero no llegó por channel, el WS falló
      - text: "Revisar stderr de a2a-mcp en busca de errores de WebSocket"
        sub: "`[a2a-mcp]` logs — buscar 'ws error', 'reconnect', 'channel'"
      - text: "`curl -s https://broker.gateway.mks2508.systems/api/broker/metrics` → deliveries count"
        sub: Métricas del broker muestran si los mensajes se entregaron
```

```dossier:matrix
groups:
  - tone: v
    title: Debe pasar
    badge: "4 checks"
    items:
      - text: ListPeers muestra ambos peers
        sub: Peer discovery funciona
      - text: "SendPeerMessage → delivered: true"
        sub: Mensajería REST funciona
      - text: "<channel> block aparece en la sesión destino"
        sub: Channel-push realtime funciona
      - text: Respuesta bidireccional
        sub: Ciclo completo A→B→A
  - tone: r
    title: Puede fallar (esperable)
    badge: "2 checks"
    items:
      - text: El channel block tarda >5s en aparecer
        sub: Posible reconnect del WS con backoff
      - text: Solo funciona el polling (CheckInbox) pero no channel-push
        sub: El WS está caído pero el REST funciona
  - tone: p
    title: Si falla → crítico
    badge: "1 check"
    items:
      - text: "SendPeerMessage no entrega (delivered: false)"
        sub: El broker no enruta mensajes — revisar peers registrados
```

```dossier:questions
items:
  - no: Q1
    title: "¿Se ven los dos peers con ListPeers?"
    chip: { text: discovery, tone: hot }
    body: Después de arrancar ambas sesiones, ejecutar ListPeers en cualquiera
    branches:
      - tone: "yes"
        label: "Sí → 2 peers"
        text: Continuar a F4 (enviar mensaje). Peer discovery funciona.
      - tone: "no"
        label: "No → solo 1 peer"
        text: "Revisar: (1) ¿están ambas sesiones apuntando al MISMO broker? (GATEWAY_BASE_URL), (2) ¿está el broker accesible desde ambas?, (3) ¿se registró el peer correctamente? (ver logs de a2a-mcp)"
  - no: Q2
    title: "¿Llega el mensaje en tiempo real?"
    chip: { text: channel-push, tone: hot }
    body: Enviar SendPeerMessage desde B a A y observar terminal A
    branches:
      - tone: "yes"
        label: "Sí → <channel> block"
        text: "UC3 VERDE. Channel-push funciona. El ecosistema a2a está sano."
      - tone: "no"
        label: "No → solo inbox"
        text: "Channel-push roto. El REST funciona pero el WS no. Revisar logs [a2a-mcp] y métricas del broker. Posible regresión en channel-push-ws.ts."
```

```dossier:sources
groups:
  - title: Código channel-push
    items:
      - label: channel-push-ws.ts
        url: apps/agent2agent-mcp/src/channel-push-ws.ts
        chip: { text: source }
      - label: mcp-server.ts
        url: apps/agent2agent-mcp/src/mcp-server.ts
        chip: { text: source }
      - label: a2a-client.ts
        url: core/packages/agent2agent-sdk/src/client/a2a-client.ts
        chip: { text: source }
      - label: cli-launcher.service.ts
        url: apps/gateway-server/src/services/cli-launcher.service.ts
        chip: { text: source }
  - title: Tests existentes
    items:
      - label: channel-push-ws.test.ts
        url: apps/agent2agent-mcp/src/__tests__/channel-push-ws.test.ts
        chip: { text: test }
      - label: channel-push-drain-race.test.ts
        url: apps/agent2agent-mcp/src/__tests__/channel-push-drain-race.test.ts
        chip: { text: test }
      - label: parity.test.ts
        url: core/packages/agent2agent-sdk/src/__tests__/parity.test.ts
        chip: { text: test }
```
