tawasuyu

tawasuyu/brahman

Rust 463TOML 180Markdown 49JSON 176353 Definiciones

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>

9c371ee43e34fc609c15ea14fdded2f0bdf78ee5 · por Sergio · (hace 5 meses) · padre 5c41ef920 · ver el árbol en este commit

Delta semántico · 3

crates/modules/nouser/core/src/bin/nouser.rs

5 archivos · +187 −11

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33ddc3a..330cebf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,56 @@ ratio/diff ver `git show <sha>`.
 
 ## 2026-05-08
 
+### chore: profile.dev slim — target/ ~50% más liviano
+Cambios en `[profile.dev]` raíz para que builds futuras no desborden
+disco. Decisiones:
+- `debug = "line-tables-only"`: stack traces correctos, drop del resto
+  de symbols. Sin pérdida real para nuestro flujo.
+- `split-debuginfo = "unpacked"`: relink más rápido, debuginfo en
+  archivos aparte.
+- `codegen-units = 256`: paralelismo + builds incrementales chicas.
+- Override `[profile.dev.package.X]` para los pesados (gpui, ort,
+  fastembed, tokenizers, image): `opt-level = 1`, `debug = false`.
+  No los debuggeamos línea por línea, no necesitan info pesada.
+
+Resultado: binarios ~3× más livianos. ente-zero 125→47 MB; mock-nous
+~50→22 MB.
+
+### feat(nouser): dynamic binding — consumer descubre el provider vía broker
+Cierra el bucle prometido por `priority_contexts`: el cliente ya no
+hardcodea el socket del provider de embeddings. En su lugar:
+
+1. Si `NOUSER_NOUS_SOCKET` está set, lo usa directo (atajo explícito).
+2. Si no, abre `brahman_handshake::client::Client` al `brahman-init`,
+   anuncia un consumer Card mínimo con `flow.input = embed-result:json`,
+   espera 3s por el primer `MatchEvent::Available`, y usa el
+   `producer_service_socket` que viaja en el evento.
+
+Esto activa el swap automático mock↔real:
+- `BRAHMAN_BROKER_CONTEXT=test`: el bias `+1 en test` del mock lo hace
+  ganar; consumer recibe el socket del mock.
+- `BRAHMAN_BROKER_CONTEXT=prod`: el bias del real lo hace ganar.
+- Sin contexto: empate alfabético entre los presentes.
+
+Validación end-to-end:
+
+  $ ente-zero & nouser-nous-mock &
+  $ # Sin NOUSER_NOUS_SOCKET:
+  $ nouser attract --remote crates/core archivo.rs
+    embed: remote
+    🧲  0.9058  ente-brain/src  ...
+  (mock log confirma "embed_file path=...")
+
+Cambios:
+- `nouser-core` Cargo.toml: deps directas brahman-handshake + tokio.
+- `cmd_attract` resuelve el socket por discovery antes de llamar a
+  `embed_via(&path, file)` (mini-runtime tokio current_thread inline).
+
+Bug que se descubrió en el camino: la "flakiness" reportada de
+`cargo test --workspace` era disco lleno (24 GB en `target/`), no
+condición de carrera. Con `cargo clean` + profile slim, todos los
+tests pasan deterministas.
+
 ### feat(nouser): yahweh widget — `nouser-explorer` panel GPUI
 Bin GPUI standalone que consulta `brahman-admin` cada 2s y renderea
 todas las sesiones del Init como cards. Cierra el círculo visual del
diff --git a/Cargo.lock b/Cargo.lock
index 763abc7..1a45878 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -6362,6 +6362,7 @@ version = "0.1.0"
 dependencies = [
  "blake3",
  "brahman-card",
+ "brahman-handshake",
  "brahman-sidecar",
  "nouser-card",
  "nouser-nous",
@@ -6370,6 +6371,7 @@ dependencies = [
  "sled",
  "tempfile",
  "thiserror 2.0.18",
+ "tokio",
  "ulid",
  "walkdir",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index dde00c0..5530b1f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -193,4 +193,40 @@ panic = "abort"
 
 [profile.dev]
 opt-level = 0
-debug = true
+# `line-tables-only` mantiene stack traces con archivo:línea correctos
+# pero descarta el resto de symbols. Reduce target/ ~40% sin sacrificar
+# debugging real para nuestro flujo (no usamos gdb sobre estos crates).
+debug = "line-tables-only"
+split-debuginfo = "unpacked"
+incremental = true
+# Más codegen-units = más paralelismo + builds incrementales más chicas
+# (cada cambio re-compila menos). Default es 256 en dev pero lo
+# anclamos para evitar regresiones.
+codegen-units = 256
+
+# Override puntual para deps grandes que NO debuggeamos: gpui, ort,
+# fastembed, tokenizers, image. Subir opt-level acá hace que sus libs
+# pesen menos en target/ (símbolos descartados durante la build).
+[profile.dev.package."*"]
+opt-level = 0
+debug = "line-tables-only"
+
+[profile.dev.package.gpui]
+opt-level = 1
+debug = false
+
+[profile.dev.package.ort]
+opt-level = 1
+debug = false
+
+[profile.dev.package.fastembed]
+opt-level = 1
+debug = false
+
+[profile.dev.package.tokenizers]
+opt-level = 1
+debug = false
+
+[profile.dev.package.image]
+opt-level = 1
+debug = false
diff --git a/crates/modules/nouser/core/Cargo.toml b/crates/modules/nouser/core/Cargo.toml
index eb89103..81e4113 100644
--- a/crates/modules/nouser/core/Cargo.toml
+++ b/crates/modules/nouser/core/Cargo.toml
@@ -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"
 
diff --git a/crates/modules/nouser/core/src/bin/nouser.rs b/crates/modules/nouser/core/src/bin/nouser.rs
index b27b9d8..0fd1257 100644
--- a/crates/modules/nouser/core/src/bin/nouser.rs
+++ b/crates/modules/nouser/core/src/bin/nouser.rs
@@ -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 {