From 25f8aeafaacb8328d49537be84adc74033600a98 Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sun, 8 Mar 2026 18:44:26 +0000 Subject: [PATCH] chore: eliminar issue 0013 de pendientes (ya movido a completed/) Co-Authored-By: Claude Sonnet 4.6 --- dev/issues/0013-hot-reload.md | 265 ---------------------------------- 1 file changed, 265 deletions(-) delete mode 100644 dev/issues/0013-hot-reload.md diff --git a/dev/issues/0013-hot-reload.md b/dev/issues/0013-hot-reload.md deleted file mode 100644 index 04b0636..0000000 --- a/dev/issues/0013-hot-reload.md +++ /dev/null @@ -1,265 +0,0 @@ -# Task 013 — Hot-Reload de Agentes Individuales - -## Objetivo - -Permitir reiniciar (recrear) un agente individual dentro del launcher sin detener -los demas agentes. El bus y el orquestador permanecen intactos porque todo sigue -en el mismo proceso. - -## Contexto - -Actualmente el launcher ejecuta todos los agentes como goroutines dentro de un -unico proceso. No hay forma de reiniciar un solo agente — hay que matar y -re-arrancar el launcher entero, lo que desconecta a todos los bots de Matrix -y rompe conversaciones en curso. - -### Por que no un proceso por agente - -El sistema de orquestacion multi-bot depende de: - -- **Bus in-process** (`shell/bus/bus.go`): Go channels, solo funciona dentro del mismo proceso. -- **Orquestador** (`shell/orchestration/`): usa el bus para `dispatchAndWait()` (request-response). -- **Deduplicacion** (`seen map`): estado compartido en memoria para evitar que multiples bots - en el mismo room procesen el mismo mensaje. -- **Interceptor**: callback sincrono que el listener de cada bot llama al orquestador. - -Separar en procesos romperia todo lo anterior. El hot-reload mantiene el proceso unico -pero recrea el agente internamente. - -## Mecanismo propuesto - -### Signal: SIGHUP + archivo de control - -1. El launcher escucha `SIGHUP` ademas de SIGINT/SIGTERM. -2. Al recibir SIGHUP, lee un archivo `run/reload.txt` que contiene el ID del agente a recargar. -3. Si el archivo no existe o esta vacio, recarga TODOS los agentes. -4. Alternativa: un comando via bus (`bus.KindReload`) enviado desde el TUI/agentctl. - -### Flujo de hot-reload - -``` -SIGHUP recibido (o comando reload via bus/TUI) - | - v -Launcher lee run/reload.txt -> agentID (o "*" para todos) - | - v -Para cada agente a recargar: - 1. Cancelar su context (ctx.cancel) -> Agent.Run() termina gracefully - 2. Esperar a que la goroutine termine (via WaitGroup o done channel) - 3. Desuscribir del bus (bus.Unsubscribe(agentID)) - 4. Re-leer config.yaml del agente - 5. Re-crear Agent con agents.New(cfg, rules, logger) - 6. Re-suscribir al bus (agent.SetBus) - 7. Re-conectar interceptor y membership notify si orquestador activo - 8. Re-registrar participante en orquestador - 9. Lanzar nueva goroutine con agent.Run(newCtx) - | - v -Log: "agent reloaded successfully" -``` - -## Plan de implementacion - -### 1. Hacer Agent cancelable individualmente - -**Archivo**: `agents/runtime.go` - -- Actualmente `Agent.Run(ctx)` recibe el context del launcher (compartido). -- Cambiar para que cada agente tenga su propio `context.WithCancel(parentCtx)`. -- Exponer un metodo `Agent.Stop()` que cancela el context hijo. -- Exponer un canal o metodo `Agent.Done() <-chan struct{}` para saber cuando termino. - -```go -type Agent struct { - // ... campos existentes ... - cancel context.CancelFunc - done chan struct{} -} - -func (a *Agent) Run(ctx context.Context) error { - ctx, a.cancel = context.WithCancel(ctx) - defer close(a.done) - // ... resto del Run existente ... -} - -func (a *Agent) Stop() { - if a.cancel != nil { - a.cancel() - } -} - -func (a *Agent) Done() <-chan struct{} { - return a.done -} -``` - -### 2. Anadir Unsubscribe al bus - -**Archivo**: `shell/bus/bus.go` - -- Nuevo metodo `Unsubscribe(id AgentID)` que elimina el canal del mapa y lo cierra. -- `listenBus()` en runtime.go debe manejar canal cerrado sin panic. - -```go -func (b *Bus) Unsubscribe(id AgentID) { - b.mu.Lock() - defer b.mu.Unlock() - if ch, ok := b.channels[id]; ok { - close(ch) - delete(b.channels, id) - } -} -``` - -### 3. Tracker de agentes en el launcher - -**Archivo**: `cmd/launcher/main.go` - -- Reemplazar el `sync.WaitGroup` actual por un registry de agentes vivos: - -```go -type runningAgent struct { - agent *agents.Agent - cancel context.CancelFunc - done chan struct{} - cfg *config.Config -} - -type agentRegistry struct { - mu sync.Mutex - agents map[string]*runningAgent -} -``` - -- Metodos: `register(id, agent)`, `stop(id)`, `reload(id, parentCtx)`, `stopAll()`. -- `reload(id)` ejecuta el flujo descrito arriba: stop -> wait -> recreate -> start. - -### 4. Handler de SIGHUP - -**Archivo**: `cmd/launcher/main.go` - -- Escuchar SIGHUP en un canal separado (no en el mismo NotifyContext de SIGINT/SIGTERM). -- Al recibir SIGHUP: - - Leer `run/reload.txt` (si existe) - - Llamar `registry.reload(id, ctx)` o `registry.reloadAll(ctx)` si es "*" - -```go -sighup := make(chan os.Signal, 1) -signal.Notify(sighup, syscall.SIGHUP) - -go func() { - for range sighup { - id := readReloadTarget("run/reload.txt") - if id == "" || id == "*" { - registry.reloadAll(ctx) - } else { - registry.reload(id, ctx) - } - } -}() -``` - -### 5. Integracion con el orquestador - -**Archivo**: `cmd/launcher/main.go` (dentro de `reload()`) - -Al recrear un agente que participa en orquestacion: - -1. El orquestador no necesita "desregistrar" al participante — basta con re-registrar - con la misma info (sobreescribe). -2. Re-llamar `SetInterceptor` y `SetMembershipNotify` en el nuevo Agent. -3. El bus.Subscribe del nuevo agente devuelve un canal nuevo — el orquestador usa - `bus.Send(agentID)` que resuelve el nuevo canal automaticamente. - -**Caso critico**: si el agente esta en medio de un `dispatchAndWait()` cuando se cancela: -- El context se cancela -> SendAndWait retorna error -- El orquestador recibe timeout/error para esa iteracion -- La respuesta parcial se pierde pero no hay corrupcion -- El orquestador puede reintentar o pasar al siguiente bot - -### 6. Integracion con el TUI - -**Archivos**: `pkg/tui/update.go`, `shell/tui/adapter.go`, `shell/process/manager.go` - -El boton "Restart" del TUI (task actual) debe cambiar de "kill+start launcher" a: - -1. Escribir el agentID en `run/reload.txt` -2. Enviar SIGHUP al proceso del launcher (`kill -HUP `) -3. Esperar un momento y refrescar estado - -```go -func (a *Adapter) restartAgent(id string) tea.Cmd { - return func() tea.Msg { - // Escribir target en reload file - os.WriteFile("run/reload.txt", []byte(id), 0644) - // Enviar SIGHUP al launcher - pid := a.mgr.UnifiedPID() - if pid > 0 { - syscall.Kill(pid, syscall.SIGHUP) - } - time.Sleep(1 * time.Second) - return puretui.MsgActionDone{AgentID: id, Action: "Restart", Err: nil} - } -} -``` - -### 7. Integracion con agentctl CLI - -**Archivo**: `cmd/agentctl/main.go` - -- Nuevo subcomando: `agentctl reload ` -- Escribe `run/reload.txt` + envia SIGHUP -- Mismo mecanismo que el TUI - -### 8. Graceful shutdown del agente - -**Archivo**: `agents/runtime.go` - -Al cancelar el context individual de un agente: - -1. El sync loop de Matrix debe detenerse limpiamente (mautrix tiene `StopSync()`) -2. Las llamadas LLM en curso deben cancelarse via context -3. La tool execution en curso debe respetar context cancellation -4. Memory/knowledge stores deben flush antes de cerrar -5. El canal del bus se cierra — `listenBus` sale del loop - -Verificar que `runtime.go:Run()` ya maneja todo esto con el context actual. -Si no, anadir cleanup explicicto. - -### 9. Tests - -- **Unit test**: `bus.Unsubscribe` no causa panic, mensajes posteriores al unsubscribe - no se pierden (retornan error). -- **Unit test**: `agentRegistry.reload()` — stop + recreate funciona. -- **Integration test**: enviar SIGHUP y verificar que solo el agente target se reinicia. -- **Orchestrator test**: agente en medio de task, se cancela, orquestador maneja el error. - -## Orden de implementacion sugerido - -1. `Agent.Stop()` + `Agent.Done()` (runtime.go) -2. `Bus.Unsubscribe()` (bus.go) -3. `agentRegistry` en launcher (main.go) -4. Handler SIGHUP (main.go) -5. Graceful shutdown verification (runtime.go) -6. Actualizar TUI adapter (adapter.go) -7. Actualizar agentctl (agentctl/main.go) -8. Tests - -## Riesgos y mitigaciones - -| Riesgo | Mitigacion | -|--------|------------| -| Race condition al cerrar canal del bus | Mutex en Unsubscribe, recover en Send | -| Crypto store de mautrix queda locked | Cerrar store explicitamente en cleanup | -| Orquestador en medio de dispatch | Context cancellation + timeout ya existente | -| Config invalido al recargar | Validar config antes de destruir agente viejo | -| Matrix sync no para limpio | Llamar StopSync() explicitamente antes de cancel | - -## Notas - -- SIGHUP es la convencion Unix para "recargar configuracion" (nginx, haproxy, etc.) -- El archivo `run/reload.txt` es efimero — se puede borrar despues de leer -- Si el launcher no esta corriendo, el TUI debe caer al comportamiento actual (start launcher) -- El orquestador NO se recarga — solo los agentes. Para recargar el orquestador - hay que reiniciar el launcher entero.