feat(explorer+daemon): discovery dinamico via broker + query socket

Cierra el "explorer encuentra al daemon de forma totalmente dinamica"
del meta-plan. La UI deja de hardcodear el socket admin: descubre al
daemon nouser via MatchEvent::Available del broker y le consulta sus
Monadas directo.

Pipeline end-to-end:
- Daemon publica engine Card con service_socket = $XDG_RUNTIME_DIR/
  nouser-engine.sock y flow.output = monad-list:json.
- Daemon binda Unix socket en ese path con listener blocking que
  sirve nouser_card::query::QueryRequest::ListMonads, responde
  ListMonadsResponse { engine, monads: Vec<MonadView> }.
- Explorer construye consumer Card con flow.input matched,
  brahman_sidecar::await_provider_blocking le devuelve el socket,
  y nouser_core::engine_socket::client::list_monads lo consulta.
- Cachea el socket; cualquier fallo de query lo invalida y la
  proxima iteracion re-descubre.

Wire types nuevos en nouser_card::query:
- QueryRequest::ListMonads
- ListMonadsResponse { engine: EngineInfo, monads: Vec<MonadView> }
- MonadView: proyeccion slim de MonadManifest SIN centroid ni
  members (KB que no tienen por que viajar cada poll).
- transport::default_socket_path() con env override.

Listener en nouser_core::engine_socket: spawn_listener + client
blocking con QueryError tipado. 3 tests integracion verdes.

Refactor explorer:
- Drop dep brahman-admin, add brahman-sidecar/nouser-card/nouser-core.
- State: socket cache + snapshot + socket_source informativo.
- TickOutcome enum desacopla la I/O del UI.

Trade-offs: polling 2s (no streaming — broker no empuja Data cards
hoy), re-discovery full en error (discovery es barato).

Tests: 10 (nouser-card +3 query) + 27 (nouser-core +3 engine_socket)
+ 4 (sidecar) verdes. Explorer compila clean.
This commit is contained in:
Sergio
2026-05-09 03:08:20 +00:00
parent b23ddf2980
commit 2ae888bc8f
9 changed files with 871 additions and 146 deletions
+62
View File
@@ -6,6 +6,68 @@ ratio/diff ver `git show <sha>`.
## 2026-05-09
### feat(explorer+daemon): discovery dinámico vía broker + query socket
La UI deja de hardcodear el socket admin: ahora descubre al daemon
nouser vía `MatchEvent::Available` del broker brahman y le consulta
sus Mónadas directo, sin pasar por brahman-admin. Cierra el "explorer
encuentra al daemon de forma totalmente dinámica" del meta-plan.
Pipeline end-to-end:
- Daemon publica engine Card con `service_socket = $XDG_RUNTIME_DIR/nouser-engine.sock`
y `flow.output = monad-list:json`.
- Daemon binda un Unix socket en ese path y monta un listener
blocking que sirve `nouser_card::query::QueryRequest::ListMonads`,
responde `ListMonadsResponse { engine, monads: Vec<MonadView> }`.
- Explorer construye un consumer Card con `flow.input = monad-list:json`
vía `brahman_sidecar::build_consumer_card`, llama
`await_provider_blocking(card, 3s)` y recibe el socket descubierto.
- Cachea ese socket; cada poll (2s) llama
`nouser_core::engine_socket::client::list_monads(socket, 2s)`.
Fallo de query → invalida cache → próximo tick re-descubre.
Wire types nuevos en `nouser_card::query`:
- `QueryRequest::ListMonads` (single variant por ahora).
- `ListMonadsResponse { engine: EngineInfo, monads: Vec<MonadView> }`.
- `MonadView`: proyección slim de `MonadManifest` SIN `centroid` ni
`members` — la UI no los necesita y eran KB por Mónada que no
tenían por qué viajar cada poll.
- `transport::default_socket_path()` con env override
`NOUSER_ENGINE_SOCKET`.
- Const `FLOW_MONAD_LIST = "monad-list"`, `FLOW_TYPE_NAME = "json"`.
Listener en `nouser_core::engine_socket`:
- `spawn_listener(config, db)` arma std::os::unix::net::UnixListener
en thread blocking dedicado. Frecuencia esperada (UI cada 2s) no
amerita tokio.
- `client::list_monads(socket, timeout)` — cliente blocking con
`QueryError` tipado (Connect / Io / Serde / Daemon / Timeout / Empty).
- 3 tests integración: roundtrip vacío, Mónadas reales, request
inválido devuelve ErrorResponse.
Refactor explorer:
- Drop dep `brahman-admin`, add deps `brahman-sidecar`, `nouser-card`,
`nouser-core`.
- State: `socket: Option<PathBuf>` cache + `snapshot: Option<ListMonadsResponse>`
+ `socket_source: "discovery"|"cache"` (sólo informativo).
- Tick: `tick(prior_socket)` separado del UI, devuelve un enum
`TickOutcome::{Ok, DiscoveryFailed, QueryFailed}`. Cualquier
fallo invalida la cache → re-discovery automática.
- Header reformulado: `Engine 'nouser_engine' · N mónada(s) ·
socket: /... (cache|discovery) · watching: /tmp/x`.
- Render pintado de un engine card + Mónadas, sin ya iterar
`BrokeredCard` del admin.
Trade-offs aceptados:
- Polling 2s (no streaming). El broker no empuja cambios de Data
cards hoy; agregar streaming requiere extender el protocolo
handshake. Para snapshot UI, polling 2s es suficiente.
- Re-descubrimiento full en cada error de query (en lugar de retry
con backoff). Discovery es barato (~ms vs broker), no vale la
pena la complejidad.
Tests: 10 (nouser-card, +3 query) + 27 (nouser-core, +3 engine_socket)
+ 4 (sidecar) verdes. Explorer compila clean.
### feat(nous-real): cache de embeddings + write-through al CAS de arje
Cierra el ciclo de la crítica del usuario: "Si un archivo no ha
cambiado su hash en el CAS, Nouser ni siquiera debería pedirle al