El daemon monta notify::recommended_watcher recursivo sobre el dir escaneado. Cada Create/Modify de archivo regular dispara: embedding → filtro por centroid_model → ranking contra centroides → log con 🧲 / · según supere DEFAULT_ATTRACTION_THRESHOLD. $ nouser daemon /tmp/x & $ vim /tmp/x/src/nuevo.rs [watcher] 🧲 /tmp/x/src/nuevo.rs → x/src (0.7470) $ echo edit >> /tmp/x/docs/n1.md [watcher] 🧲 /tmp/x/docs/n1.md → x/docs (0.8169) Mecánica: - DB pasa a Arc<Mutex<MonadDb>> para sharing con thread watcher. - Watcher en thread dedicado nouser-watcher; reacciona sólo a Create/Modify, ignora Access/Metadata-only. - react_to_change(path, metadata, db) computa embedding, filtra por centroid_model, busca best attraction. - No re-publica al broker ni muta DB — sólo observa y narra. La invalidación selectiva (re-cluster + replace + diff publish) queda para futuro. Limitación conocida: notify emite múltiples eventos por edición (Create + Modify, etc.). Sin debounce el watcher reporta varias veces. Aceptable para demo; producción conviene debounce ~100ms por path. Esto cierra la Fase C del plan post-reporte: el sistema "se siente" vivo. Tocar un archivo en vim y ver inmediatamente la atracción calculada cumple el meta-mensaje "Mónada Viva". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
44 KiB
Changelog
Registro cronológico de cambios sustantivos en el monorepo Brahman. Cada
entrada lista las acciones concretas tras un commit; para detalles de
ratio/diff ver git show <sha>.
2026-05-09
feat(nouser): notify watcher — el sistema reacciona en tiempo real
El daemon ahora monta un notify::recommended_watcher recursivo
sobre el directorio. Cada Create/Modify de archivo regular
dispara: embedding del archivo, filtro por centroid_model, ranking
contra centroides existentes, log con marker 🧲 / · según supere
el umbral de atracción.
$ nouser daemon /tmp/x &
en otra terminal:
$ vim /tmp/x/src/nuevo.rs
daemon log:
[watcher] 🧲 /tmp/x/src/nuevo.rs → x/src (0.7470)
$ echo "edit" >> /tmp/x/docs/n1.md [watcher] 🧲 /tmp/x/docs/n1.md → x/docs (0.8169)
Mecánica:
- DB pasa a
Arc<Mutex<MonadDb>>para sharing con el thread del watcher. - Watcher en thread dedicado (
nouser-watcher); reacciona sólo a Create/Modify, ignora Access/Metadata-only. react_to_change(path, metadata, db)computa embedding, filtra porcentroid_model, busca best attraction.- No re-publica al broker ni muta DB — sólo observa y narra. La invalidación selectiva (re-cluster + replace_monads + diff publish) queda como work futuro.
Limitación conocida: notify emite múltiples eventos por una sola
edición (Create + Modify, etc.). Sin debounce, el watcher reporta
varias veces. Aceptable para demo; production conviene debounce
~100ms por path.
Tests: 7 (card) + 24 (core) verdes, 0 errores, 0 warnings.
feat(nouser): hidratación del daemon vía sled + path_hint
El daemon ya no recomputa ciegamente al arrancar. Si la DB tiene
Mónadas previas con centroid_model válido, las publica instantáneo
y el re-scan reusa sus IDs vía path_hint.
Schema:
MonadManifest.path_hint: Option<String>— identidad estable derivada del origen (paraby_directory, el parent dir canónico). Permite reusar ULID across re-scans.
Algoritmo (cluster):
- Nueva fn
cluster::by_directory_hydrated(files, min_files, prior: Option<&MonadDb>). Cuando hayprior, busca Mónada con mismopath_hintY mismocentroid_model; si la encuentra, reusaid,lineageycreated_at_ms. by_directoryqueda como wrapper sin hidratación (back-compat).
Daemon (cmd_daemon):
- Open sled si NOUSER_DB_PATH existe.
- Publica las Mónadas previas con
centroid_modelválido (las inválidas se descartan con log explícito). - Re-scan +
by_directory_hydrated(prior=&db). - Sólo spawnea sidecars para Mónadas con id que NO estaba en la hidratación inicial. Los path_hints existentes preservan identidad, evitando duplicados en el broker.
- Persiste el set actualizado.
Validación end-to-end:
$ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core
arranque 1: DB vacía
re-scan 102 archivos → 5 mónadas
1 ente + 5 mónadas vivas (5 nuevas vs hidratación)
$ NOUSER_DB_PATH=/tmp/h.sled nouser daemon crates/core
arranque 2: DB poblada
hidratadas 5 mónadas previas en O(1)
re-scan 102 archivos → 5 mónadas
1 ente + 5 mónadas vivas (0 nuevas vs hidratación)
Costo del arranque 2: ~0.06s user CPU. Antes (sin hidratación) era re-scan + cluster + spawn x N — segundos enteros para árboles grandes.
Tests: 7 (card) + 24 (core) verdes.
feat(nouser): centroid_model — versionado de embeddings
Protege contra el bug silencioso de mezclar centroides de modelos distintos (mock 32-d vs real 384-d), que daba scores sin sentido.
MonadManifest.centroid_model: Option<String>taggea qué modelo produjo elcentroid.None= legacy pre-versioning.nouser_core::embed::MODEL_ID = "nouser-pseudo-32d". El cluster lo setea en cada Mónada que genera.nouser-nous-mockreusa la misma constante (use nouser_core::embed::MODEL_ID); produce vectores idénticos al cluster local, así que reportar el mismo ID es honesto.nouser-nous-realreporta"real-fastembed-allMiniLML6V2-384d"(dim distinta, semántica distinta).cmd_attractahora:- Captura el
model_iddel embedding del target (local o remote). - Filtra Mónadas cuyo
centroid_modelno matchee. - Reporta
embed: <source> (<model>)yskipped: N mónadas con centroid_model distintocuando descarta.
- Captura el
Resultado operativo: cambiar de mock a real (vía
BRAHMAN_BROKER_CONTEXT=prod) hace que attract filtre las Mónadas
viejas con cero score en lugar de fingir que las puede comparar.
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:
- Si
NOUSER_NOUS_SOCKETestá set, lo usa directo (atajo explícito). - Si no, abre
brahman_handshake::client::Clientalbrahman-init, anuncia un consumer Card mínimo conflow.input = embed-result:json, espera 3s por el primerMatchEvent::Available, y usa elproducer_service_socketque viaja en el evento.
Esto activa el swap automático mock↔real:
BRAHMAN_BROKER_CONTEXT=test: el bias+1 en testdel 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-coreCargo.toml: deps directas brahman-handshake + tokio.cmd_attractresuelve el socket por discovery antes de llamar aembed_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
ecosistema brahman.
- Crate nuevo
crates/apps/nouser-explorer(deps: brahman-admin, brahman-card, gpui). - Ventana 900×640 con header del estado del Init, banner de error cuando no conecta, y lista de cards (una por sesión).
- Cada card muestra: kind + label + lifecycle, ULID corto, summary (si data), keywords, lens hint, service_socket si está, y refs (RelationshipKind → target_label). El borde izquierdo coloreado diferencia ente (azul) de data (lavanda).
cx.spawn(async move |this, cx| { … })corre el loop de refresh en el GPUI executor;query_blockingse usa porque GPUI no provee un runtime tokio.- Nuevo helper en brahman-admin:
client::query_blocking(path)— versión sync dequery(), para callers con su propio executor.
Uso:
$ ente-zero & nouser daemon crates/core & $ cargo run -p nouser-explorer
ventana muestra ~6 cards en vivo, refrescando cada 2s.
cargo check --workspace: 0 errores, 0 warnings.
feat(nouser): persistencia sled write-through del MonadDb
MonadDb ahora soporta backend dual:
MonadDb::new()→ memoria pura (default, back-compat).MonadDb::open(path)→ sled-backed con cache en memoria. Carga contenido existente al abrir; cadainsert_*hace write-through (cache + sled).
Diseño:
- 2 trees sled:
filesymonads. - Wire format: serde_json (ergonomía + inspectability con sled-cli; los manifests son chicos, JSON gana sobre postcard aquí).
- Reads SIEMPRE desde la cache — sled se consulta sólo al abrir.
replace_monads()purga el tree de sled antes de escribir.
Bin nouser: nueva env var NOUSER_DB_PATH. Si está set, persiste
en esa ruta; si no, in-memory:
$ NOUSER_DB_PATH=/tmp/monads.sled nouser scan crates/core scan: 102 archivos en crates/core, 5 mónadas $ ls /tmp/monads.sled blobs conf $ NOUSER_DB_PATH=/tmp/monads.sled nouser scan crates/core
segunda corrida re-escribe la DB con el nuevo scan
Tests nuevos en db.rs:
persistence_roundtrip— escribe, cierra, reabre, datos están.replace_monads_purges_persistent_tree— replace limpia el tree.
24 tests en nouser-core (era 22, +2).
feat(sidecar): Phase B-3 — SidecarPool consolida en un runtime
Antes: cada spawn(card) creaba un thread + tokio runtime propio.
Para módulos que publican muchas sesiones (nouser daemon con 50+
Mónadas) eso es 50 threads + 50 runtimes. Ahora: un thread + un
runtime tokio current_thread que hostea N tasks de sidecar.
API nueva (aditiva, no rompe spawn/spawn_with_handle):
let pool = SidecarPool::new()?; pool.spawn(card1); pool.spawn(card2); pool.spawn_conscious(card_wit, wit); pool.spawn_with_config(SidecarConfig::new(c).with_wit(w)); // pool drop = todas las sesiones cierran.
run_client se hace pública para que el pool pueda enqueuar tasks
externos al runtime con handle.spawn(run_client(config)).
nouser daemon migrado al pool. Verificación con ps -L:
$ ps -L -p $(pidof nouser) LWP CMD 28817 nouser # main thread 28819 brahman-sidecar # pool thread (todas las sesiones)
Antes serían 6+ LWP (1 main + N sesiones); ahora 2 fijos sin importar cuántas Mónadas se publiquen.
feat: Crossreferencia — Card.references como grafo del fractal
Las Cards ahora declaran sus relaciones con otras Cards. El Engine posee Mónadas; las Mónadas declaran que son poseídas por el Engine. La UI puede cruzar el grafo sin discovery especial.
brahman-card:RelationshipKind { Owns, OwnedBy, Processes, ProcessedBy, Sibling }.CardReference { kind, target_id, target_label }—target_labeles cache del label en el momento de declarar (la UI puede pintar sin resolver).Card.references: Vec<CardReference>y espejo enWireCard. ConversionesFrompropagan.
brahman-broker::BrokeredCardpropagareferences.brahman-statusimprime cada referencia:ref OwnedBy → label (id).- nouser daemon: cada Mónada que publica añade
RelationshipKind::OwnedByapuntando al engine. La declaración es unilateral; el engine no necesita conocer las IDs de antemano.
Validación end-to-end:
$ ente-zero & nouser daemon crates/core $ brahman-status Sessions (6): [ente] ... brahman.nouser_engine [data] ... brahman-handshake/src ref OwnedBy → brahman.nouser_engine (01K...) summary: 6 archivos... [data] ... ente-brain/src ref OwnedBy → brahman.nouser_engine (01K...) ...
feat: Phase D-3 + D-4 — service_socket en Card, providers coexisten
Cierra el ciclo del swap automático de Nous (mock↔real):
- Schema (
brahman-card):Card.service_socket: Option<PathBuf>y espejo enWireCard. ConversionesFrompropagan. Es el path del data plane (distinto del socket del Init); cualquier consumer que matchee con esta Card puede conectar directo sin discovery adicional. - Broker (
brahman-broker):BrokeredCardpropagaservice_socketdesde la Card. Sin participación en el matching — sólo metadata para los observadores. - MatchEvent (
brahman-handshake): nuevo campoproducer_service_socket: Option<PathBuf>. Cuando el server emiteAvailable, busca laBrokeredCarddel productor en el broker y copia suservice_socket. El consumer recibe la ruta completa para conectar. - Transport (
nouser-nous):provider_socket_path(provider: &str)devuelvenouser-nous-{provider}.sockpor default — mock y real coexisten en sockets distintos (Phase D-4).default_socket_path()conserva el comportamiento single-provider. - Providers: mock declara
service_socket = /run/user/X/nouser-nous-mock.sock; real declaranouser-nous-real.sock. La Card se construye DESPUÉS del bind para que el path declarado sea el real. - Status:
brahman-statusimprimesocket:por sesión cuando está presente.
Validación end-to-end:
$ ente-zero & nouser-nous-mock & nouser-nous-real & $ ls /run/user/1001/nouser-nous-*.sock nouser-nous-mock.sock nouser-nous-real.sock
$ brahman-status Sessions (2): [ente] ... nouser.nous_real socket: /run/user/1001/nouser-nous-real.sock in embed-request: Primitive { name: "json" } out embed-result: Primitive { name: "json" } [ente] ... nouser.nous_mock socket: /run/user/1001/nouser-nous-mock.sock in embed-request, out embed-result
Pendientes para futuro (no críticos):
- nouser-core attract --remote todavía usa NOUSER_NOUS_SOCKET hardcoded
o
default_socket_path(). El siguiente paso es subscribirse al MatchEvent del broker y usarproducer_service_socketdirecto — con esoBRAHMAN_BROKER_CONTEXT=test/prodswapea provider sin tocar al consumer.
refactor(nouser): labels de Mónada con 2 componentes del path
Resuelve la fricción visual de monorepos donde múltiples Mónadas se
llamaban "src". Nueva función label_from_path toma los últimos hasta
2 componentes normales del path y los une con /.
$ nouser scan crates/core [01K..] brahman-admin/src card=5 [01K..] brahman-handshake/src card=6 [01K..] ente-brain/src card=11 [01K..] ente-kernel/src card=4 ...
Tests añadidos: label_from_root_only_one_component,
label_from_deep_path_takes_last_two. Tests existentes actualizados
con los nuevos labels.
feat(nouser): Phase D-2 — proveedor Nous real (LLM) detrás de feature flag
Cierra el ciclo del módulo Nous: existe un proveedor que produce
embeddings reales con un modelo LLM, mientras que cargo build sin
features sigue siendo liviano (no descarga ni compila ML deps).
Crate nuevo:
-
crates/modules/nouser/nous-real: bin con dos modos según feature.- Sin feature (default): stub. Bin compila en ~10s, arranca,
sidecarea a brahman-init declarando la Card de real-nous, escucha
en el socket Nous, y rechaza toda request con
ErrorResponse { error: "compilado sin la feature embeddings. Rebuild con cargo build -p nouser-nous-real --features embeddings" }.cargo build --workspacesigue siendo limpio. - Con
--features embeddings: pullsfastembed = "4". Ese crate arrastraort 2.0.0-rc.9(ONNX Runtime con binarios descargados por Cargo) +tokenizers 0.21+ ~30 deps más. Compila en ~50s. Modelo default:all-MiniLM-L6-v2(384-d, descargado a~/.cache/fastembedla primera vez). EmbedText: pasa el texto al modelo, devuelve vector 384-d.EmbedFile: lee primeros 8KiB con UTF-8 lossy, embed como texto. Para binarios el resultado no es semánticamente útil — caller decide.Ping: devuelvemodel_idyembed_dimreales.
- Sin feature (default): stub. Bin compila en ~10s, arranca,
sidecarea a brahman-init declarando la Card de real-nous, escucha
en el socket Nous, y rechaza toda request con
-
Card de real-nous:
- label
nouser.nous_real(distinto del mock para coexistir). priority_contexts.prod = { priority_offset: +1 }. En contexto prod gana sobre el mock; entestel mock gana por su propio+1. Sin contexto activo, empate alfabético entre ambos.
- label
Validación end-to-end con modelo real:
$ cargo build -p nouser-nous-real --features embeddings # ~50s
$ ente-zero & nouser-nous-real &
$ # probe vía python al socket Unix:
$ echo '{"kind":"embed_text","payload":{"text":"hello brahman"}}'
| python3 -c "..." | head
model: real-fastembed-allMiniLML6V2-384d
elapsed_ms: 8
embed_dim: 384
first 5 values: [0.0034, -0.0036, 0.0078, -0.0218, -0.0162]
Tradeoff conocido: las dimensiones del mock (32-d) y real (384-d) son incompatibles. Cambiar de proveedor invalida los centroides cacheados de Mónadas. Documentar como "limpiar DB al cambiar proveedor".
Workspace state:
- cargo build --workspace sigue limpio sin features (no ML).
- cargo build -p nouser-nous-real --features embeddings funciona.
- 0 errores, 0 warnings en ambos modos.
Pendientes para D-3 / futuro:
- Discovery de socket: hoy el consumer hardcodea NOUSER_NOUS_SOCKET. Para que el broker brahman elija real vs mock per-contexto, falta inyectar el socket del provider electo en el MatchEvent o exponer un broker query "dame el socket de la sesión X".
- Coexistencia: hoy los dos providers compiten por el mismo socket path por default. Habría que parametrizarlos a sockets distintos cuando coexistan.
feat(nouser): Phase D — proveedor Nous mock + cliente remoto
Cierra el patrón "Nous como módulo aparte intercambiable": el contrato
del proveedor de embeddings vive en su crate, el mock determinístico
implementa ese contrato sirviéndolo por Unix socket, y nouser-core
sabe consumirlo remotamente. El switch entre mock y real (futuro) se
hará vía priority_contexts en el broker.
Crates nuevos:
crates/modules/nouser/nous: contrato compartido. TiposEmbedRequest,RequestKind { EmbedFile, EmbedText, Ping },EmbedFilePayload,EmbedTextPayload,EmbedResponse,PingResponse,ErrorResponse. Wire format: line-delimited JSON por Unix socket, single-shot per conexión. Constants para los nombres de flow (embed-request/embed-result) y el tipo (json). Helpertransport::default_socket_path()con env varNOUSER_NOUS_SOCKET.crates/modules/nouser/nous-mock: binnouser-nous-mock. Sidecarea a brahman-init con Card kind=Ente declarando los flowsembed-request:json/embed-result:jsony unpriority_contexts.test = { priority_offset: +1 }(gana sobre cualquier real-nous en contexto test). Bind del socket Nous, accept loop, despacha porRequestKind. EmbedFile usanouser_core::embed::embed(los pseudo-embeddings de Phase C). Modelo:mock-pseudo-32d.
Cambios:
nouser-core: dep nuevanouser-nous. Subcomandoattractahora acepta--remoteque abre un socket UnixStream blocking, envía unEmbedRequesty lee la response. Imprimeembed: local|remotepara que se vea cuál ruta corrió.
Validación end-to-end (un solo terminal, varios procesos):
$ ente-zero & $ nouser-nous-mock & $ NOUSER_MIN_FILES=5 nouser daemon crates/core & $ brahman-status
Sessions (7): [ente] nouser.nous_mock flows: embed-request, embed-result [ente] brahman.nouser_engine [data] src summary: 6 archivos en crates/core/brahman-handshake/src [data] graph summary: 7 archivos en crates/core/ente-zero/src/graph ...
$ nouser attract --remote crates/core <archivo.rs> embed: remote 🧲 0.9058 src ...
Mock log: "embed_file path=crates/modules/nouser/core/src/embed.rs"
Bug encontrado y corregido en el camino:
ContextBiastenía#[serde(skip_serializing_if = ...)]en sus campos. Postcard NO soporta skip-condicional (formato no self-describing): el serializer omitía bytes que el deserializer esperaba, rompiendo la wire de cualquier Card conpriority_contextspoblada.- Fix: removidos los
skip_serializing_ifdeContextBias. JSON pretty ahora emite{"pin_to": null, "priority_offset": 0}en lugar de objeto vacío. Trade-off aceptado por compatibilidad de wire. - Test nuevo en brahman-card:
wirecard_postcard_with_priority_contextsque ejercita el roundtrip completo postcard.
Tests acumulados: 75 (card 12 +1 nuevo, broker 15, handshake 9, card-wit 4, admin 0, nouser-card 7, nouser-core 20, nouser-nous 2). cargo check --workspace: 0 errores, 0 warnings.
Próximo natural: Phase D-2 — real-nous con un modelo ONNX/Llama de
text-embedding. La infraestructura ya está lista: declara la misma
Card con priority_contexts.prod = { priority_offset: +1 } y el
swap es transparente para el consumer.
feat(nouser): Phase C — pseudo-embeddings + atracción por centroide
El "imán semántico" matemático del diseño Kairos, sin LLM. Cada archivo se proyecta a un vector 32-d derivado de sus metadatos; cada Mónada calcula su centroide; archivos nuevos se asignan por cosine similarity contra los centroides existentes.
Cambios:
- nouser-core dep nueva:
blake3(hash determinista de strings). crates/modules/nouser/core/src/embed.rs:EMBED_DIM = 32. Estructura del vector:- dims 0..8: blake3(extension) → identidad de tipo
- dims 8..16: blake3(parent_dir) → identidad de contenedor
- dims 16..24: blake3(file_stem) → identidad léxica
- dims 24..28: tamaño (log + flags)
- dims 28..32: mtime (escala día + cíclicas)
- Tip clave: bytes del hash se centran a
[-1, 1](no[0, 1]). Sin centrar, dos vectores hash random tendrían cosine ~0.75 espuria; centrados, expectativa ≈ 0 entre no-relacionados. - APIs:
embed,cosine_similarity,centroid,cohesion,attraction_score,best_attraction.DEFAULT_ATTRACTION_THRESHOLD = 0.7.
cluster::by_directoryahora computa el centroide de cada Mónada (promedio de embeddings de los miembros, L2-normalizado) y lo guarda enMonadManifest.centroid. El centroide viaja al brahman-status víaDataFacet.centroid→ ahora se ven los Vec reales por cada Mónada.- bin nouser nuevo subcomando:
attract <dir> <file>.- Escanea el dir, embeda el archivo objetivo, ranking de afinidad contra todas las Mónadas con centroide.
- Marca 🧲 si la mejor supera el umbral,
·si es la mejor pero debajo, espacio en blanco para el resto.
Validación end-to-end:
$ nouser attract crates/core crates/modules/nouser/core/src/embed.rs ranking de atracción (cosine similarity): 🧲 0.9058 [01K..] src (11 archivos en crates/core/ente-brain/src) 0.8984 [01K..] src (6 archivos en crates/core/brahman-handshake/src) 0.8918 [01K..] src (5 archivos en crates/core/ente-zero/src) ...
$ nouser attract crates/core crates/modules/nouser/core/Cargo.toml ranking: 0.3427 [01K..] graph (7 archivos en crates/core/ente-zero/src/graph) ... (mejor score 0.3427 < umbral 0.7000 — el archivo no se 'pega')
Tests: 20 en nouser-core (era 13, +7 de embed). Total acumulado: 73 (card 11, broker 15, handshake codec+tr 2 + integ 7, card-wit 4, admin 0, nouser-card 7, nouser-core 20, ente-card 0). cargo check --workspace: 0 errores, 0 warnings.
Próximo: Phase D — nouser-nous, módulo aparte para LLM real.
Mock-nous determinista (basado en estos pseudo-embeddings) en
BRAHMAN_BROKER_CONTEXT=test; real-nous en prod. El switch lo hace
el broker via priority_contexts sin tocar nada más.
feat(nouser): Phase B-2 — daemon que publica Mónadas al Init
Cierra la unificación: el nouser daemon se sidecarea como Ente y
publica cada Mónada como su propia sesión Data. Un solo
brahman-status muestra procesos y datos en la misma lista, exactamente
como buscaba el diseño.
Cambios:
crates/modules/nouser/core/Cargo.toml: deps nuevasbrahman-cardybrahman-sidecar.crates/modules/nouser/core/src/bin/nouser.rs: subcomandodaemon <dir>.- Spawna un sidecar para el "engine" (
brahman.nouser_engine, kind=Ente) — el ser que produce y administra Mónadas. - Scan + cluster del dir.
- Para cada Mónada, llama
monad.to_brahman_card()y spawnea un sidecar (kind=Data). Cada Mónada es una sesión brahman propia con su ULID estable. - Park del thread principal: los sidecars siguen pingueando.
- Spawna un sidecar para el "engine" (
Validación end-to-end:
$ ente-zero & $ NOUSER_MIN_FILES=5 nouser daemon crates/core & $ brahman-status
Sessions (6): [ente] ... brahman.nouser_engine lifecycle=Daemon [data] ... src summary: 5 archivos en crates/core/brahman-admin/src members: 5 (dispersion=0.00) lens hint: code [data] ... src summary: 11 archivos en crates/core/ente-brain/src ... [data] ... graph summary: 7 archivos en crates/core/ente-zero/src/graph
El protocolo de presentación es uno solo: la Card. La función — anunciar identidad, exponer metadata, ser descubierto — es idéntica para procesos vivos y agrupaciones de datos. La UI lo ve como una lista uniforme.
Costo conocido: cada Mónada consume un thread + tokio runtime current_thread (legacy del sidecar API). Para muchas Mónadas (>50) conviene consolidar en un único runtime con N tasks. Defer a Phase B-3.
Pendientes propuestos:
- B-3: consolidar todos los sidecars en un único runtime tokio para no spawnear N threads.
- C: pseudo-embeddings + atracción por centroide.
- D: módulo
nouser-nouspara LLM, swappable por priority_contexts. - Polish: labels con 2-3 componentes del path.
- Crossreferencia: que un Ente pueda anunciar "estoy procesando la Mónada X" y la Mónada anuncie "Ente Y me está procesando".
cargo check --workspace: 0 errores, 0 warnings.
feat: Phase B-1 — unificación ontológica de Cards (Ente ↔ Data)
La Card es el protocolo de presentación del ecosistema, no sólo de
los procesos. Una Mónada Nouser y un Ente Brahman son ambos "entidades
que se presentan"; el consumidor (UI, broker, admin) discrimina por
kind cuando importa, pero todos hablan el mismo idioma.
Cambios:
-
brahman-card:CardKind { Ente (default), Data }. Conserva back-compat: Cards existentes sonEntepor default.DataFacet { summary, keywords, centroid, member_count, dispersion, presentation_hint }. Liviano para el wire — listas grandes (members, embeddings completos) se consultan al daemon dueño bajo demanda.Card.kindyCard.data: Option<DataFacet>agregados. WireCard espeja, conversionesFrompropagan.- Default impl actualizado.
-
brahman-broker::BrokeredCard: propagakindydatadesde la Card registrada. No afecta el matching (sigue siendo por TypeRef + priority + pin_to); permite a observadores discriminar sin re-query. -
nouser-card: depende ahora debrahman-card. Nuevo métodoMonadManifest::to_brahman_card()que proyecta:- id, label, lineage → directos.
- payload Virtual, supervision Delegate, lifecycle Daemon (placeholder semántico — la Mónada no se ejecuta).
- kind = Data.
- data = Some(DataFacet) con summary, keywords, centroide,
member_count, entropy → dispersion, y un
presentation_hintderivado delLens(Code→"code",Gallery→"gallery", etc.). - Test nuevo:
projects_to_brahman_card.
-
brahman-status: cada sesión muestra ahora[ente]o[data]como prefijo. Para sesionesdata, render adicional con summary, members- dispersion, keywords y lens hint.
Resultado: la UI (yahweh, brahman-status, futuro explorer) ve una sola
lista uniforme. No tiene que saber si está mirando un proceso o un
cúmulo de datos — sólo lee el Card y se adapta por kind.
Tests acumulados: 59 (card 11, broker 15, handshake codec+transport 2 + integ 7, card-wit 4, admin 0, nouser-card 7, nouser-core 13). cargo check --workspace: 0 errores, 0 warnings.
Próximo: Phase B-2 — bin nouser daemon <dir> que sidecarea cada
Mónada como una sesión brahman, publicándola al broker. Brahman-status
las verá junto a los entes.
feat(nouser): Phase A — mecanismo determinista de Mónadas
Primer trozo del módulo Nouser (Kairos): explorador de Mónadas como "imanes semánticos" sobre el filesystem. Phase A cubre el 90% de los casos sin tocar IA — sólo metadatos y heurísticas.
Crates nuevos:
crates/modules/nouser/card:MonadManifest(la Tarjeta de Presentación de una Mónada — espejo conceptual debrahman::Cardpero para datos, no para procesos runtime). Campos: id (Ulid), label, summary, centroid (vacío en Phase A), keywords, cardinality, entropy [0,1], dominant_lens, pins, members, timestamps, extensions (forward-compat). 6 tests de validación + JSON roundtrip.crates/modules/nouser/core: pipeline determinista.scanner: walkdir →Vec<FileEntry>con metadatos (path, size, mtime, extension). Skipea hidden por default. Configurable max depth y follow_links.cluster::by_directory: agrupa por parent dir, mínimo 3 archivos para promover a Mónada (configurable). Calcula keywords (top-N extensiones por frecuencia + alfabético), eligeLensdominante (Code/Gallery/Markdown/Database/Grid) según extensión más frecuente, computa entropía de Shannon normalizada [0,1].db:MonadDben memoria con índices BTreeMap files/monads yresolve_members(monad_id)que filtra IDs huérfanos. Phase B traerá persistencia.- bin
nouser: subcomandosscan <dir>,show <dir> <prefix>,json <dir>. Env varNOUSER_MIN_FILESpara tunear el threshold. - 13 tests (4 scanner + 6 cluster + 3 db).
Demo end-to-end:
$ nouser scan crates scan: 255 archivos en crates, 19 mónadas (min_files=3) [01KR4C13] src card=12 ent=0.00 lens=Code keywords: rs [01KR4C13] tests card=14 ent=0.00 lens=Code keywords: rs [01KR4C13] fixtures card=5 ent=0.00 lens=Grid keywords: rhai ...
$ nouser show crates 01KR4C Monad 01KR4C1370DVF6NMTW6SECNXAF label: src summary: 4 archivos en crates/modules/nouser/core/src (ext: rs) cardinality: 4 entropy: 0.0000 lens: Code members (4): 4132 bytes crates/modules/nouser/core/src/db.rs ...
Pendientes para próximas fases (anotados, no urgentes):
- Phase B: bin
nouser daemonque sidecarea a brahman-init declarando flows (scan-request:json→monad-update:json). - Phase C: pseudo-embeddings deterministas (hash de path/ext/size a 32-d) + atracción por centroide via cosine similarity. Implementa el "imán" sin LLM.
- Phase D: módulo
nouser-nousaparte para el LLM real (Llama/ONNX). Enpriority_contexts.testel Init pinea amock-nous(embeddings determinísticos); enprodareal-nous. - Polish: labels de Mónada incluir 2-3 componentes del path para
desambiguar
src/repetidos en monorepo.
Workspace: 0 errores, 0 warnings. Tests acumulados: 58 (card 11, broker 15, handshake codec+transport 2 + integ 7, card-wit 4, admin 0, nouser-card 6, nouser-core 13).
feat(broker): priority contexts — biases per-contexto operativo
brahman-card::ContextBias { pin_to: Option<String>, priority_offset: i8 }declara un override per-contexto.Card.priority_contexts: BTreeMap<String, ContextBias>y mismo enWireCard(cruza el wire). Las conversionesFromlo propagan.BrokerConfig.current_context: Option<String>. Cuando el broker corre bajo un contexto y una Card declara biases para ese nombre, se aplican:- Como consumidor:
pin_tosobreescribe elFlow.pin_toestático. - Como productor:
priority_offsetse suma a la priority base (clamp en[Low=0, Critical=3]) para el ranking.
- Como consumidor:
BrokeredCardpropagapriority_contexts.find_producer_forusaeffective_priority(card)yeffective_pin(card, input)antes de los tiebreaks.brahman-admin::AdminConfig.current_context+StatusSnapshot.current_contextespejan el contexto activo.brahman-statuslo imprime comoContext: <nombre>justo debajo deInit: ....ente-zeroleeBRAHMAN_BROKER_CONTEXTenv var y la propaga al broker y al admin. Sin var, biases per-contexto inactivos.- 4 tests nuevos en brahman-broker:
context_priority_offset_lifts_producer_above_alphabetic_winner,context_pin_to_overrides_static_pin,unknown_context_no_op,priority_offset_clamps_to_critical. - Validación end-to-end:
BRAHMAN_BROKER_CONTEXT=test ente-zero→brahman-statusmuestraContext: test.
feat(card): WireCard + extensions — forward-compat sin romper postcard
Card.extensions: BTreeMap<String, serde_json::Value>restaurado con#[serde(flatten, default, skip_serializing_if = is_empty)]. Los campos JSON/TOML desconocidos sobreviven el roundtrip de archivos.- Nuevo
WireCard: proyección postcard-friendly (sinextensions,genesis: Vec<WireCard>recursivo). ConversionesFrom<Card>yFrom<WireCard>con descarte/recreación de extensions. brahman-handshake::Hello.cardpasa deCardaWireCard. Client hacecard.into()antes de enviar; Server hacehello.card.into()para volver a Card antes de validar/registrar.- 3 tests nuevos en brahman-card:
extensions_preserved_in_json_roundtrip,wire_card_roundtrip_strips_extensions,wire_card_postcard_friendly(postcard encode/decode efectivo). - brahman-card gana
postcardcomo dev-dep para el último test. - Contrato documentado: extensions = anotaciones locales que NO cruzan al Init; sólo viven en archivos.
9420eae chore: limpia warnings dead-code en arje (commit del usuario)
ente-zero/src/events.rs:#![allow(dead_code)]a nivel módulo — es vocabulario de eventos con variantes/campos reservados para flujos no cableados aún (CapabilityRequested, ShutdownReason::Signal, CapabilityGrant::{Granted, Denied, QuotaExceeded}, ExitStatus fields).ente-zero/src/graph/mod.rs: comentado el re-export ahora innecesario deSHUTDOWN_GRACE.DEFAULT_GRANT_TTLcon#[allow(dead_code)]- nota "reservado para capability granting".
ente-zero/src/graph/capabilities.rs:renew_grantcon#[allow(dead_code)](capability renewal pendiente).ente-kernel/src/surface.rs: drop deuse anyhow::Context(no se usaba).ente-hostnamed-compat/src/main.rs: drop deConnection(no se usaba).ente-polkit-compat/src/main.rs:PolicyDecision.sourcecon#[allow(dead_code)](sólo aparece enDebugpara logging).cargo check --workspace: 17 warnings → 0.
feat(sidecar): WIT al sidecar — módulos conscientes vivos
brahman-card::WitInterfacederivaSerialize,Deserialize,PartialEq,Eqpara cruzar el wire postcard.brahman-handshake::Hellollevawit: Option<WitInterface>. Server usaResolvedCard::from_consciouscuando viene presente,from_agnosticcuando no.brahman-handshake::Client::connectqueda como wrapper agnóstico deconnect_with(path, card, wit: Option<WitInterface>).brahman-broker::Broker::registerahora tomaOption<WitInterface>como tercer arg.BrokeredCardguarda el wit. 25 sitios de tests actualizados con, None.brahman-sidecar::SidecarConfigcon campowit. Helpers nuevos:SidecarConfig::new(card).with_wit(wit)yspawn_conscious(card, wit). El logattachedreportaconscious=true|false.brahman-statusmuestra marker 🧠 + secciónwit:(package/world, imports, exports) por sesión consciente.- Example nuevo
crates/shared/brahman-sidecar/examples/presence-conscious.rs: toma label + path .wit (defaultshared_wit/protocol.wit), parsea con brahman-card-wit, spawna sidecar consciente. - Validado end-to-end:
$ presence-conscious demo.conscious shared_wit/protocol.wit & $ brahman-status Sessions (1): 01K... demo.conscious 🧠 lifecycle=Daemon wit: brahman:protocol@0.1.0 / module imports: types, handshake, lifecycle exports: run
feat(core): brahman-card-wit — extractor opcional de contratos WIT
- Crate nuevo
crates/core/brahman-card-witconwit-parser = "0.230". - API:
parse_wit(source)yparse_wit_file(path)devuelvenVec<WitInterface>(uno porworlddeclarado). - Interfaces importadas/exportadas (no sólo funciones) se resuelven
por nombre via
resolve.interfaces[id].name. - Example
crates/core/brahman-card-wit/examples/brahman-wit-info.rsCLI:brahman-wit-info shared_wit/protocol.wit→ lista paquete, worlds, imports y exports. - 4 tests: inline, archivo real (
shared_wit/protocol.wit), parse error, world vacío. - Validado contra
protocol.wit: detecta worldsmoduleyadmin-hostcon sus imports/exports correctos.
7b589b8 chore: agrega CHANGELOG.md retroactivo
CHANGELOG.mden la raíz con los 11 commits previos documentados acción por acción. A partir de este punto, cada cambio sustantivo actualiza también este archivo en el mismo commit.
8a83a26 feat(handshake): notificación push de matches
- Frame
MatchEvent { kind: Available | Lost, ... }añadido al protocolo. Session::run_post_handshakeusatokio::select!para multiplexar reads del cliente y un canalmpscpush del server.- Server:
SessionTxTable(Arc<Mutex<HashMap<SessionId, Sender>>>) yLastMatchespara diff por sesión.broadcast_match_diffscorre tras cadaregisteryunregister, emite sólo los cambios. - Capacity del canal push: 32 (ephemeral,
try_sendnon-blocking). - Client:
VecDeque<MatchEvent>interno,take_event()(non-blocking) yawait_event(timeout).ping()ahora drena MatchEvents intermedios hasta encontrar el Pong. - Example
crates/core/brahman-handshake/examples/subscriber.rs. - Test
match_event_pushed_on_producer_arrival(handshake integ 6→7).
70a7a0d feat: segundo módulo (nakui) + admin API + brahman-status
- Crate nuevo
crates/shared/brahman-sidecar(DRY del thread + tokio + ping loop). API:spawn(card)/spawn_with_handle(config). nakuicmd_run llamabrahman_sidecar::spawnantes derun_server. Card: lifecycle Daemon, supervision Restart, flowcommand(json) /report(json).- Crate nuevo
crates/core/brahman-adminconStatusSnapshotJSON line-delim,AdminServeryclient::query. - ente-zero levanta también el AdminServer en
primordial_loop. - Example
crates/shared/brahman-sidecar/examples/presence.rs(módulo dummy long-lived parametrizable por label). - Example
crates/core/brahman-admin/examples/brahman-status.rs(CLI que pretty-printa el snapshot). brahman-broker:BrokeredCardahora incluyelifecycle.EndpointyMatchderivanSerialize/Deserialize. NuevoBroker::cards()iterador.brahman-card:pub use ::ulidpara que módulos no dependan de ulid.- yahweh-shell migrado al sidecar compartido (96→53 LOC).
595f68e feat(yahweh-shell): primer módulo brahman vivo
- yahweh-shell spawnea sidecar antes de
Application::new(). - Card declarada: label
brahman.ui_engine, lifecycle Widget, supervision Delegate, payload Virtual, flow inputrender-data(json) / outputuser-intent(json). - Sidecar en thread aparte con tokio current_thread runtime, desacoplado del runtime GPUI.
df9d10c feat(ente-zero): enchufa el handshake server al Init real
- ente-zero levanta
brahman_handshake::server::Server::bindenprimordial_loopdespués del ente-bus, con degradación grácil si bind falla (mismo patrón que uevents). - Nuevo módulo
brahman-handshake/src/transport.rs: helperdefault_socket_path()con resoluciónBRAHMAN_INIT_SOCKET→XDG_RUNTIME_DIR→TMPDIR. - Example
crates/core/brahman-handshake/examples/probe.rs. - Validación end-to-end manual: probe contra ente-zero vivo
imprime
HelloAck: session=... init_attached=true.
07d77a3 feat(handshake): integra el broker con el ciclo de sesiones
ServerConfigaceptaOption<Arc<Mutex<Broker>>>.register_sessionindexa la Card en el broker y laSessionRegistryantes de emitir HelloAck.Session::handlerefactor ado_handshake → run_post_handshake → cleanupcon cleanup unificado (broker + sessions).- Tests integ nuevos:
broker_registers_and_unregisters_with_sessionybroker_matches_two_live_modules. - Fix colateral:
brahman-card::TypeRefpasa de internally-tagged (#[serde(tag = "kind")]) a externally-tagged. Postcard no soporta internally-tagged en formatos no self-describing. JSON cambia de{"kind":"primitive","name":"x"}a{"primitive":{"name":"x"}}.
5091106 feat(core): brahman-broker — matching híbrido
- Crate nuevo
crates/core/brahman-broker. - 3 estrategias de matching:
Exact,Structural,ExactThenStructural(default). DevuelvenMatch::viacon la estrategia que ganó. - Override
pin_to: el consumer pide un productor por label; si la pista no resuelve, cae en type-search. - Tiebreak por
Card.prioritydesc, luegolabelasc (estable y determinista). - API:
register,unregister,find_producer_for,all_matches,cards,sessions,len,is_empty. - 11 tests (matching, pin_to, priority, no-self-loops, all-matches).
814390f feat(core): brahman-handshake — protocolo runtime
- Crate nuevo
crates/core/brahman-handshakecon server y client Rust↔Rust sobre Unix socket. - Frames length-prefixed (4 bytes LE) + cuerpo postcard.
- Mensajes:
Hello,HelloAck,Ping,Pong,Farewell,Error. MAX_FRAME_BYTES = 4 MiBpara evitar reservas absurdas.- Tradeoff: drop
extensions/extrade Card por incompat postcard ↔serde_json::Value. Forward-compat queda enschema_version+protocol_versionnegotiation. - 4 tests integ + 1 unit en codec.
ed0e973 refactor(arje): migra ente-card a re-export de brahman-card
ente-card/src/lib.rsreescrito como crate-shim de re-export (327 LOC → 25 LOC).EntityCard≡brahman_card::Cardpor type alias.ente-card/Cargo.toml: deps reducidas abrahman-card.CardimplDefault(Ulid::nil(), label vacío) para que..Default::default()funcione en struct-literals.- 4 sitios en
ente-zero/src/seed.rsactualizados con..Default::default()para los campos aditivos. - Los 21 consumidores arje compilan sin tocar fuente.
0feba74 feat(core): brahman-card — Tarjeta canónica híbrida
- Crate nuevo
crates/core/brahman-card. - Hereda de arje:
id: Ulid,lineage,Capabilitytipado,Payload::{Wasm, Native, Virtual, Legacy},SomaSpec(namespaces, cgroups, rlimits, cpu_affinity),Supervision(Restart con backoff, OneShot, Delegate),genesisrecursivo. - Aditivo brahman:
Permissionsenumerados (NetworkingPolicy,FsPolicy,IpcPolicy),Lifecycleortogonal a Supervision,Priorityde scheduling,FlowsconTypeRefdiscriminado (Primitive | Wit),pin_toopcional. TrustLevelderivado dePermissions(no declarado).ResolvedCard { card, wit: Option<WitInterface>, trust }.- Soporta JSON (canónico) + TOML (auto-detectado por extensión).
- 8 tests incluido
arje_seed_format_compatibleque valida que el JSON de arje sigue parseando con defaults para los aditivos.
4d50bfc chore: absorbe nakui (ERP matemático) en modules/nakui
~/nakui→crates/modules/nakui/{core,modules}.core/: el cratenakui-corecon 4 bins (nakui, demo, inventory_demo, sales_demo) y tests.modules/{inventory,sales,treasury}/: data declarativa (nsmc.json,schema.k,morphisms/) que el crate consume. No son crates Cargo.- Deps directas (no
workspace = true): thiserror v1, surrealdb, rhai, petgraph. No conflicto con el resto del workspace.
53dbdf0 chore: monorepo inicial con arje + minga + yahweh absorbidos
- 45 crates absorbidos en 4 ejes:
crates/core/: 24 crates de arje (Init systemd-compatible:ente-card,ente-zero,ente-kernel,ente-bus,ente-cas,ente-soma,ente-wasm,ente-snapshot,ente-brain,ente-echo,ente-policy-provider, + 12*-compat).crates/modules/semantic_dht/: 5 crates de minga (minga-corecon AST/CAS/MST,minga-p2pcon libp2p Kad,minga-store,minga-vfs,minga-cli).crates/modules/ui_engine/: 11 crates de yahweh (libs/{core, theme, bus, providers}, widgets/{tree, splitter, tabs, tiled, container_core, text_input}).crates/apps/: 5 crates de yahweh (file_explorer, database_explorer, text_viewer, image_viewer, yahweh-shell).
shared_wit/protocol.witcon handshake/lifecycle inicial.Cargo.tomlunificado: thiserror bumped a 2 (transparente para arje), tokio "full", paths intra-workspace de yahweh redirigidos.cargo check --workspace: 0 errores (sólo dead-code warnings preexistentes en ente-zero).