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 `. ## 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, Box> { + 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, Box> { 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, Box(&response) { return Ok(resp.embedding); } @@ -366,6 +388,70 @@ fn remote_embed(file: &nouser_card::FileEntry) -> Result, Box Result> { + 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 {