feat(brahman-handshake): ListSessions endpoint + cliente + UI broker-explorer

Iter 20. Nuevo flujo end-to-end para observabilidad: cualquier módulo
conectado puede pedir al broker la lista de sesiones activas y mostrar
labels + flows in/out por cada una.

brahman-handshake/messages:
- Frame::ListSessions(ListSessions{session}) → Frame::SessionList(SessionList{entries}).
- SessionEntry: session, label, schema_version, outputs, inputs, conscious.

brahman-handshake/server:
- run_post_handshake pasa SessionRegistry a handle_inbound_frame.
- build_session_list helper proyecta el snapshot bajo lock.
- Validación session_id mismatched → Unauthorized.

brahman-handshake/client:
- Client::list_sessions() async, drena MatchEvents intermedios al
  pending_events buffer, mismo patrón que ping().

brahman-sidecar/discovery:
- list_sessions / list_sessions_blocking arman Card observer mínima,
  piden, Farewell.

brahman-broker-explorer:
- Poll-tick agrega list_sessions_blocking cuando broker está UP*.
- stat_card "Sesiones activas" con count + items ordenados por Ulid:
  label · in:[flows] out:[flows]  (wit)?.

Test list_sessions_returns_currently_registered: 3 clientes
conectados, observer pide list, verifica labels + schema_version
+ conscious=false. 24 handshake tests + sidecar + broker-explorer
verde.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-10 15:40:33 +00:00
parent 37e40073ef
commit 99cd685dc1
8 changed files with 378 additions and 10 deletions
@@ -142,9 +142,61 @@ pub fn await_provider_blocking(
.enable_time()
.build()
.map_err(|e| ConsumerError::Runtime(e.to_string()))?;
rt.block_on(await_provider(consumer_card, timeout))
}
/// Conecta al brahman-init con una Card observer (sin inputs ni
/// outputs) y pide la lista de sesiones activas. Útil para
/// herramientas de observabilidad (broker-explorer, CLIs).
///
/// El observer se identifica con `observer_label`. La sesión se
/// cierra con Farewell antes de retornar (best-effort).
pub async fn list_sessions(
observer_label: impl Into<String>,
) -> Result<brahman_handshake::messages::SessionList, ConsumerError> {
let init_path = transport::default_socket_path();
// Card mínima sin flow.input/output: el observer no participa en
// matching, sólo establece sesión para poder consultar.
let card = Card {
payload: Payload::Virtual,
supervision: Supervision::OneShot,
lifecycle: Lifecycle::Oneshot,
priority: Priority::Normal,
kind: CardKind::Ente,
flow: Flows {
input: vec![],
output: vec![],
},
..Card::new(observer_label)
};
let mut client = Client::connect(&init_path, card)
.await
.map_err(|source| ConsumerError::Connect {
socket: init_path.clone(),
source,
})?;
let list = client.list_sessions().await?;
let _ = client.farewell().await;
Ok(list)
}
/// Wrapper bloqueante de [`list_sessions`]. Idéntico patrón a
/// `await_provider_blocking`: runtime current_thread efímero.
pub fn list_sessions_blocking(
observer_label: impl Into<String>,
) -> Result<brahman_handshake::messages::SessionList, ConsumerError> {
let label = observer_label.into();
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()
.map_err(|e| ConsumerError::Runtime(e.to_string()))?;
rt.block_on(list_sessions(label))
}
fn describe_first_input(card: &Card) -> (String, String) {
match card.flow.input.first() {
Some(flow) => {
+2 -1
View File
@@ -18,7 +18,8 @@
pub mod discovery;
pub use discovery::{
await_provider, await_provider_blocking, build_consumer_card, ConsumerError,
await_provider, await_provider_blocking, build_consumer_card, list_sessions,
list_sessions_blocking, ConsumerError,
};
use std::collections::HashMap;