feat: profile.dev slim + dynamic binding del consumer Nous

Dos piezas del plan post-reporte, en un commit por estar acopladas
(ambas tocan cómo se construye y conecta el sistema):

profile.dev slim:
- debug = "line-tables-only" + split-debuginfo unpacked +
  codegen-units 256 en [profile.dev].
- Override [profile.dev.package.{gpui,ort,fastembed,tokenizers,image}]
  con opt-level=1, debug=false para los pesados que no debuggeamos.
- Resultado: binarios ~3× más livianos. ente-zero 125→47 MB;
  mock-nous ~50→22 MB. target/ futuro mucho más manejable.

dynamic binding (cierra priority_contexts):
- nouser-core Cargo.toml: deps directas brahman-handshake + tokio.
- cmd_attract refactor:
  - Si NOUSER_NOUS_SOCKET está set, atajo explícito (compat).
  - Si no, abre Client al brahman-init, anuncia consumer Card con
    flow.input = embed-result:json, espera 3s por MatchEvent::Available,
    usa producer_service_socket del evento.
- discover_producer_socket() es async; cmd_attract usa runtime tokio
  current_thread inline (block_on).
- embed_via(path, file) se separa como helper sync para la RPC.

Validación end-to-end:
  $ ente-zero & nouser-nous-mock &
  $ nouser attract --remote crates/core archivo.rs
    🧲  0.9058  ente-brain/src  ...
  (mock log: "embed_file path=archivo.rs" — discovery activo)

Con esto BRAHMAN_BROKER_CONTEXT=test/prod swappea el provider sin que
el consumer toque nada — la promesa de priority_contexts es real.

Bug colateral resuelto: la "flakiness" del cargo test --workspace era
disco lleno (24 GB en target/), no condición de carrera. Con
cargo clean + profile slim, tests deterministas.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-05-08 22:23:44 +00:00
parent 5c41ef920d
commit 9c371ee43e
5 changed files with 187 additions and 11 deletions
+2
View File
@@ -12,12 +12,14 @@ description = "Nouser — explorador de Mónadas: scanner, clustering determinis
nouser-card = { path = "../card" }
nouser-nous = { path = "../nous" }
brahman-card = { path = "../../../core/brahman-card" }
brahman-handshake = { path = "../../../core/brahman-handshake" }
brahman-sidecar = { path = "../../../shared/brahman-sidecar" }
blake3 = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sled = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
ulid = { workspace = true }
walkdir = "2"
+96 -10
View File
@@ -321,22 +321,45 @@ fn cmd_attract(args: &[String]) -> Cmd {
Ok(())
}
/// Cliente blocking del socket nouser-nous. Conecta, envía un
/// `EmbedRequest`, lee la response, devuelve el vector. Single-shot.
/// Pipeline completo del modo `--remote`:
/// 1. Si `NOUSER_NOUS_SOCKET` está set, lo usa directo (override
/// explícito, atajo para tests).
/// 2. Si no, abre Client al brahman-init, anuncia un consumer Card
/// con `flow.input = embed-result:json`, espera el primer
/// `MatchEvent::Available`, y usa el `producer_service_socket`
/// del evento. Esto activa la lógica de `priority_contexts`: si
/// el broker corre bajo `BRAHMAN_BROKER_CONTEXT=test/prod`, el
/// proveedor electo cambia sin que este consumer toque su código.
/// 3. Con el socket resuelto, dispara la RPC `EmbedFile`.
fn remote_embed(file: &nouser_card::FileEntry) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
if let Ok(explicit) = std::env::var("NOUSER_NOUS_SOCKET") {
let sock = std::path::PathBuf::from(explicit);
return embed_via(&sock, file);
}
// Discovery vía broker: el consumer se conecta al brahman-init y
// aprende qué proveedor matchea su input.
let rt = tokio::runtime::Builder::new_current_thread()
.enable_io()
.enable_time()
.build()?;
let producer_sock = rt.block_on(discover_producer_socket())?;
embed_via(&producer_sock, file)
}
/// RPC blocking contra un socket nouser-nous concreto.
fn embed_via(
sock_path: &std::path::Path,
file: &nouser_card::FileEntry,
) -> Result<Vec<f32>, Box<dyn std::error::Error>> {
use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
let sock_path = nouser_nous::transport::default_socket_path();
if !sock_path.exists() {
return Err(format!(
"socket nouser-nous no existe en {} — corrió nouser-nous-mock?",
sock_path.display()
)
.into());
return Err(format!("socket no existe: {}", sock_path.display()).into());
}
let mut stream = UnixStream::connect(&sock_path)?;
let mut stream = UnixStream::connect(sock_path)?;
let req = nouser_nous::EmbedRequest {
kind: nouser_nous::RequestKind::EmbedFile,
payload: serde_json::to_value(nouser_nous::EmbedFilePayload {
@@ -358,7 +381,6 @@ fn remote_embed(file: &nouser_card::FileEntry) -> Result<Vec<f32>, Box<dyn std::
return Err("nouser-nous cerró sin respuesta".into());
}
// Intentamos primero como response normal; si falla, como error.
if let Ok(resp) = serde_json::from_str::<nouser_nous::EmbedResponse>(&response) {
return Ok(resp.embedding);
}
@@ -366,6 +388,70 @@ fn remote_embed(file: &nouser_card::FileEntry) -> Result<Vec<f32>, Box<dyn std::
Err(format!("nouser-nous: {}", err.error).into())
}
/// Conecta al brahman-init, anuncia un consumer Card y espera el
/// primer `MatchEvent::Available`. Devuelve el `producer_service_socket`
/// que el broker emite. Timeout 3s.
async fn discover_producer_socket() -> Result<std::path::PathBuf, Box<dyn std::error::Error>> {
use brahman_card::{
ulid::Ulid, Card, CardKind, Flow, Flows, Lifecycle, Payload, Priority, Supervision,
TypeRef,
};
use brahman_handshake::client::Client;
use brahman_handshake::messages::MatchEventKind;
let consumer_card = Card {
schema_version: brahman_card::CARD_SCHEMA_VERSION,
id: Ulid::new(),
label: "nouser.attract-cli".into(),
payload: Payload::Virtual,
supervision: Supervision::OneShot,
lifecycle: Lifecycle::Oneshot,
priority: Priority::Normal,
kind: CardKind::Ente,
flow: Flows {
input: vec![Flow {
name: nouser_nous::FLOW_EMBED_RESULT.into(),
ty: TypeRef::Primitive {
name: nouser_nous::FLOW_TYPE_NAME.into(),
},
pin_to: None,
}],
output: vec![],
},
..Default::default()
};
let init_path = brahman_handshake::transport::default_socket_path();
let mut client = Client::connect(&init_path, consumer_card)
.await
.map_err(|e| format!("conectar a brahman-init en {}: {e}", init_path.display()))?;
// El broker empuja MatchEvents tras registrar la sesión. Iteramos
// hasta encontrar Available; ignoramos Lost (no aplica al arranque).
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3);
let socket = loop {
let remaining = deadline.saturating_duration_since(std::time::Instant::now());
if remaining.is_zero() {
break None;
}
match client.await_event(remaining).await? {
Some(ev) if ev.kind == MatchEventKind::Available => {
break ev.producer_service_socket;
}
Some(_) => continue, // Lost u otros — seguir esperando
None => break None,
}
};
let _ = client.farewell().await; // best-effort cleanup
socket.ok_or_else(|| {
"ningún proveedor con service_socket matcheó el input embed-result \
(¿está corriendo nouser-nous-mock o nouser-nous-real?)"
.into()
})
}
/// Card del propio engine (kind=Ente). Es el "ser" que produce y
/// administra Mónadas; aparece en brahman-status junto a sus Mónadas.
fn build_engine_card() -> brahman_card::Card {