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:
@@ -6,6 +6,56 @@ ratio/diff ver `git show <sha>`.
|
|||||||
|
|
||||||
## 2026-05-08
|
## 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
|
### feat(nouser): yahweh widget — `nouser-explorer` panel GPUI
|
||||||
Bin GPUI standalone que consulta `brahman-admin` cada 2s y renderea
|
Bin GPUI standalone que consulta `brahman-admin` cada 2s y renderea
|
||||||
todas las sesiones del Init como cards. Cierra el círculo visual del
|
todas las sesiones del Init como cards. Cierra el círculo visual del
|
||||||
|
|||||||
Generated
+2
@@ -6362,6 +6362,7 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"blake3",
|
"blake3",
|
||||||
"brahman-card",
|
"brahman-card",
|
||||||
|
"brahman-handshake",
|
||||||
"brahman-sidecar",
|
"brahman-sidecar",
|
||||||
"nouser-card",
|
"nouser-card",
|
||||||
"nouser-nous",
|
"nouser-nous",
|
||||||
@@ -6370,6 +6371,7 @@ dependencies = [
|
|||||||
"sled",
|
"sled",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
"ulid",
|
"ulid",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|||||||
+37
-1
@@ -193,4 +193,40 @@ panic = "abort"
|
|||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
opt-level = 0
|
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
|
||||||
|
|||||||
@@ -12,12 +12,14 @@ description = "Nouser — explorador de Mónadas: scanner, clustering determinis
|
|||||||
nouser-card = { path = "../card" }
|
nouser-card = { path = "../card" }
|
||||||
nouser-nous = { path = "../nous" }
|
nouser-nous = { path = "../nous" }
|
||||||
brahman-card = { path = "../../../core/brahman-card" }
|
brahman-card = { path = "../../../core/brahman-card" }
|
||||||
|
brahman-handshake = { path = "../../../core/brahman-handshake" }
|
||||||
brahman-sidecar = { path = "../../../shared/brahman-sidecar" }
|
brahman-sidecar = { path = "../../../shared/brahman-sidecar" }
|
||||||
blake3 = { workspace = true }
|
blake3 = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
sled = { workspace = true }
|
sled = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
tokio = { workspace = true }
|
||||||
ulid = { workspace = true }
|
ulid = { workspace = true }
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
|
|
||||||
|
|||||||
@@ -321,22 +321,45 @@ fn cmd_attract(args: &[String]) -> Cmd {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cliente blocking del socket nouser-nous. Conecta, envía un
|
/// Pipeline completo del modo `--remote`:
|
||||||
/// `EmbedRequest`, lee la response, devuelve el vector. Single-shot.
|
/// 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>> {
|
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::io::{BufRead, BufReader, Write};
|
||||||
use std::os::unix::net::UnixStream;
|
use std::os::unix::net::UnixStream;
|
||||||
|
|
||||||
let sock_path = nouser_nous::transport::default_socket_path();
|
|
||||||
if !sock_path.exists() {
|
if !sock_path.exists() {
|
||||||
return Err(format!(
|
return Err(format!("socket no existe: {}", sock_path.display()).into());
|
||||||
"socket nouser-nous no existe en {} — corrió nouser-nous-mock?",
|
|
||||||
sock_path.display()
|
|
||||||
)
|
|
||||||
.into());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut stream = UnixStream::connect(&sock_path)?;
|
let mut stream = UnixStream::connect(sock_path)?;
|
||||||
let req = nouser_nous::EmbedRequest {
|
let req = nouser_nous::EmbedRequest {
|
||||||
kind: nouser_nous::RequestKind::EmbedFile,
|
kind: nouser_nous::RequestKind::EmbedFile,
|
||||||
payload: serde_json::to_value(nouser_nous::EmbedFilePayload {
|
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());
|
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) {
|
if let Ok(resp) = serde_json::from_str::<nouser_nous::EmbedResponse>(&response) {
|
||||||
return Ok(resp.embedding);
|
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())
|
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
|
/// Card del propio engine (kind=Ente). Es el "ser" que produce y
|
||||||
/// administra Mónadas; aparece en brahman-status junto a sus Mónadas.
|
/// administra Mónadas; aparece en brahman-status junto a sus Mónadas.
|
||||||
fn build_engine_card() -> brahman_card::Card {
|
fn build_engine_card() -> brahman_card::Card {
|
||||||
|
|||||||
Reference in New Issue
Block a user