# Report 0001 — unibus: servicio LAN + Fase 2 (transporte sobre el bus)

- **Fecha:** 06/06/2026
- **Autor:** Claude (Opus 4.8)
- **Ámbito:** `projects/message_bus/apps/unibus` (sub-repo `dataforge/unibus`) y `~/DataProyects/Github/agents_and_robots` (Gitea `egutierrez/agents_and_robots`)
- **Estado:** done (con un gap acotado documentado en Fase 2)

## Resumen

Dos entregas. **A)** `membershipd` pasa de proceso manual loopback a service systemd real
alcanzable por la LAN: flag `--bind` que gobierna a la vez el HTTP de control y el NATS
embebido, unit systemd-user con `Restart=always`, bloque `service:` completo y health
verificado desde la IP de la LAN. Esto desbloquea que un teléfono/PC remoto conecte al bus.
**B)** Fase 2 (branch-by-abstraction): se introduce una interfaz `Transport` + un tipo
`InboundMessage` neutral (sin mautrix), se desacoplan las firmas del core de agentes, se
implementa un `unibusTransport` sobre `pkg/client` de unibus, y un feature flag elige Matrix
vs unibus por bot. Pre-requisito cumplido: el `Frame` de unibus gana threading/reply/reaction
de forma aditiva.

Trabajo en ramas TBD: `issue/fase2-transport-service` (unibus) y `issue/unibus-transport`
(agents_and_robots). Master nunca tocado directamente.

## Cambios

### A — Servicio (unibus)

| Cambio | Archivo | Por qué |
|---|---|---|
| Flag `--bind` (default 127.0.0.1) aplicado a HTTP + NATS | `cmd/membershipd/main.go` | Un solo flag gobierna la alcanzabilidad de ambos planos; `0.0.0.0` expone a la LAN |
| `embeddednats.StartHost(storeDir, host, port)` (y `Start` backward-compatible) | `pkg/embeddednats/embeddednats.go` | Controlar la interfaz del NATS embebido sin romper playground/tests |
| systemd-user unit `Restart=always` | `deploy/unibus-membershipd.service` | Service persistente; `on-failure` NO reinicia tras un SIGTERM limpio (exit 0) — el gotcha de `sqlite_api` |
| `install.sh` idempotente + `README.md` | `deploy/` | build + symlink unit + enable/restart en un comando; operar/health documentado |
| Bloque `service:` completo | `app.md` | `systemd-user`, `restart_policy: always`, health `/healthz`, `is_local_only: false`; valida `fn doctor services-spec` |

### Pre-Fase 2 — Frame threading (unibus, aditivo)

| Cambio | Archivo | Por qué |
|---|---|---|
| Campos `ThreadID`, `ReplyTo` (omitempty) + tipo `REACT` | `pkg/frame/frame.go` | Bots de chat necesitan replies, hilos y reacciones |
| `PublishReply`, `React` (refactor de `Publish` a `publishFrame` compartido) | `pkg/client/client.go` | API de threading; la reacción viaja en el payload cifrado (confidencial en rooms E2E) |
| Tests: round-trip threaded, back-compat de wire/firma, error path, reply+react E2E | `pkg/frame/*_test.go`, `pkg/client/client_test.go` | golden + edge + error path |

Aditivo de verdad: un frame sin threading serializa **sin** las claves `thr`/`re`, así que su
wire y su firma Ed25519 son idénticos a antes → no rompe `Subscribe`/`Publish` ni los tests
existentes.

### B — Fase 2 (agents_and_robots)

| Cambio | Archivo | Por qué |
|---|---|---|
| `Transport` interface + `InboundMessage` neutral + `Select`/`FlagEnabled` | `pkg/transport/` | Seam neutral (cero mautrix) entre el core del agente y el fabric de mensajería |
| Flag `unibus-transport` (default off) | `dev/feature_flags.json` | Convivencia: con el flag ON cada bot opta in; el resto sigue en Matrix |
| `handleEvent(…, *event.Event)` → `handleInbound(…, transport.InboundMessage)` (Agent + Robot) | `agents/handler.go`, `agents/robot.go` | Quitar `*event.Event` de las firmas; `inboundToMsgCtx` es el único punto de conversión a `decision.MessageContext` |
| `RawMatrixClient() *mautrix.Client` → `Scanner() RoomScanner` | `agents/runtime.go`, `cmd/launcher/main.go` | Quitar el handle al cliente mautrix de la API pública; capability read-only |
| Listener entrega `InboundMessage` (no el evento) | `shell/matrix/listener.go` | El evento mautrix ya no cruza la frontera al core |
| `unibusTransport` + `DemoEchoHandler` (módulo Go anidado) | `shell/transportunibus/` | Implementa `Transport` sobre `pkg/client`; subjects `agent.<name>.{in,out}` |

## Verificación

### A — Servicio (evidencia ejecutable)

```
$ systemctl --user is-active unibus-membershipd.service
active

$ systemctl --user status unibus-membershipd.service   # (extracto)
Active: active (running)
 ExecStart=…/membershipd --bind 0.0.0.0

$ ss -tln | grep -E ':8470|:4250'
LISTEN 0  4096  *:8470  *:*        # HTTP control en todas las interfaces
LISTEN 0  4096  *:4250  *:*        # NATS data plane en todas las interfaces

$ curl -fsS -w " [HTTP %{http_code}]\n" http://127.0.0.1:8470/healthz
{"status":"ok"} [HTTP 200]

$ curl -fsS -w " [HTTP %{http_code}]\n" http://192.168.1.129:8470/healthz   # IP LAN
{"status":"ok"} [HTTP 200]

$ ./fn doctor services-spec | grep unibus
OK  unibus  systemd-user  8470  /healthz  unibus-membershipd.service  lucas-linux  -
```

**Cliente NATS conecta por IP LAN y publica/recibe** (worker→chat, ambos contra
`nats://192.168.1.129:4250` + `http://192.168.1.129:8470`, vía el service systemd):

```
[chat] subscribed to "proc.test.ticks"; waiting for messages
[proc.test.ticks] DGCob41a: tick 1 @ 2026-06-06T16:07:16Z
[proc.test.ticks] DGCob41a: tick 2 @ 2026-06-06T16:07:17Z
[proc.test.ticks] DGCob41a: tick 3 @ 2026-06-06T16:07:18Z
[proc.test.ticks] DGCob41a: tick 4 @ 2026-06-06T16:07:19Z
```

### Pre-Fase 2 — Frame (unibus)

```
$ cd projects/message_bus/apps/unibus
$ CGO_ENABLED=0 go build ./... && CGO_ENABLED=0 go vet ./...   # exit 0
$ CGO_ENABLED=0 go test ./...
ok  github.com/enmanuel/unibus/pkg/frame
ok  github.com/enmanuel/unibus/pkg/client
ok  github.com/enmanuel/unibus/pkg/membership
```

Tests nuevos: `TestThreadingRoundTrip` (golden), `TestNonThreadedWireBackCompat` (edge:
asegura que `thr`/`re` no aparecen en frames no-threaded), `TestUnmarshalRejectsGarbage`
(error path), `TestThreadedReplyAndReaction` (reply + reacción E2E en room cifrada ModeMatrix).

### B — Fase 2 (agents_and_robots)

Toda la compilación/test del repo `agents` se hace con `-tags goolm` (olm puro-Go) porque
`libolm` (CGO) no está instalado en esta máquina; en producción se usa `libolm`.

```
# Módulo anidado del transporte unibus (flag ON / camino unibus):
$ cd shell/transportunibus && go test -tags goolm ./...
ok  github.com/enmanuel/agents/shell/transportunibus
  # TestDemoBotOverUnibus: un user-peer publica en agent.demo.in; el bot, movido por
  # unibusTransport, recibe un InboundMessage y responde "echo: hola bus" / "pong" en
  # agent.demo.out. TestRunStopsOnContextCancel: lifecycle (error path).

# Módulo principal (flag OFF / camino Matrix sigue vivo):
$ go build -tags goolm ./...            # exit 0
$ go vet -tags goolm ./agents/ ./shell/matrix/ ./pkg/transport/   # exit 0
$ go test -tags goolm ./pkg/transport/ ./agents/
ok  github.com/enmanuel/agents/pkg/transport   # TestSelect (flag ON/OFF) + TestFlagEnabled
ok  github.com/enmanuel/agents/agents          # core decoplado compila+pasa
$ go test -tags goolm ./...                     # 26 paquetes ok, 1 FAIL pre-existente (ver gaps)
```

## Gaps / pendientes

1. **Segundo host físico real para A.** El round-trip de A se probó desde esta misma máquina
   contra su IP de LAN `192.168.1.129` (no loopback), lo que ejercita el bind `0.0.0.0`. No
   había un segundo PC/teléfono disponible para conectar desde otra máquina; como los sockets
   escuchan en `*` (todas las interfaces, confirmado por `ss`), el comportamiento desde un host
   remoto es idéntico. El agente de frontend puede confirmarlo desde el móvil.

2. **El Agent "pesado" todavía no corre sobre unibus.** Conflicto real de dependencias entre
   ecosistemas: `unibus` depende de `fn-registry`, cuyo módulo raíz exige
   `maunium.net/go/mautrix v0.28.0`, incompatible con el `shell/matrix` del repo `agents`
   (escrito para `v0.21.1`; MVS forzaría v0.28 y rompe 3 call-sites de la API mautrix). Por eso
   el `unibusTransport` vive en un **módulo Go anidado** (`shell/transportunibus/go.mod`): tira
   de la mautrix nueva transitivamente sin recompilar el código Matrix del padre, y ambos
   ecosistemas conviven. Consecuencia: el core del Agent (módulo principal) no puede instanciar
   `unibusTransport` directamente sin reintroducir el conflicto. El seam neutral
   (`pkg/transport.Transport`) sí está y el bot demo corre sobre el bus; **migrar el Agent
   completo a unibus requiere primero subir `agents` a mautrix v0.28** (migración aparte, fuera
   de alcance de esta tanda).

3. **`Scanner() RoomScanner` sigue devolviendo tipos mautrix** en sus métodos
   (`*mautrix.RespJoinedRooms`, …). El escaneo de rooms es una capability intrínsecamente de
   Matrix (enumera rooms de Matrix); la neutralización quita el handle al cliente crudo y lo
   sustituye por una interfaz read-only mínima, pero no puede ser 100% neutral porque el unibus
   no participa en ese escaneo.

4. **`libolm` no instalado** en esta máquina → el repo `agents` se compila/test con
   `-tags goolm` (olm puro-Go). En producción se usa `libolm`. Pendiente: instalar
   `libolm-dev` si se quiere validar el path libolm localmente.

5. **Test pre-existente en rojo, ajeno a este trabajo:** `shell/logger/TestCleanOldLogs` falla
   porque es dependiente de fecha (espera logs de marzo 2026 que la limpieza por antigüedad
   borra estando hoy a 06/06/2026). Ya fallaba en el baseline antes de tocar nada; no toqué
   `shell/logger`. Issue separado recomendado.

6. **Sin push a remoto.** El trabajo queda consolidado en `master` local de ambos repos; el
   push a Gitea queda para `/full-git-push` del operador.
   - `agents_and_robots`: `master` = merge `--no-ff` de `issue/unibus-transport` (commit
     `aa595f5`), build + tests verdes; rama borrada.
   - `unibus`: **condición de carrera entre dos agentes en el mismo repo.** El agente de
     frontend trabajaba en paralelo en el mismo working tree de unibus y nuestros `git checkout`
     se interleavearon: mis commits A1/A2/A3 (`--bind`, deploy, service block) acabaron
     directamente en `master`, y los de threading + bump (`2209283`, `69079d1`) cayeron sobre la
     rama del otro agente `issue/frontend-web-movil`. Como esos dos commits son lineales sobre
     mi A3 (`b2e6712`) y NO contenían trabajo del otro agente, consolidé `master` con
     `git merge --ff-only 69079d1` (sin duplicar commits, sin tocar la rama del frontend). Estado
     final: `master` de unibus contiene TODO mi trabajo (A1-A3 + threading + bump 0.3.0), tests
     `pkg/...` verdes, working tree con el WIP del frontend (mobile/playground/android/web)
     intacto. **Lección operativa:** dos agentes Claude en el mismo repo comparten HEAD/índice;
     hace falta worktrees separados (`git worktree`) o repos clonados por agente para evitarlo.
