Fix: Vigilante del auto-update funciona por fin (Agent 2.13.3, causa raíz cazada)
Descripción
Versión: Agent 2.13.3 (APP v1.45.9) — commit be210c6 (05-07-2026 11:24 UTC)
Componente: Watchdog (vigilante) post-swap del auto-update (terminal/agent/core/updater.py)
Cambio: Corrección de flags de creación de proceso Windows que hacía morir silenciosamente al vigilante.
El watchdog que debería relanzar automáticamente el Agente si una actualización falla o revertir a la versión anterior nunca llegaba a ejecutarse. Causa raíz: conflicto entre flags DETACHED_PROCESS y CREATE_NO_WINDOW en subprocess.Popen, que privaba a PowerShell de una consola válida → muere antes de la primera línea del script.
Lo que cambia
Antes (Agent ≤2.13.2)
# updater.py:_launch_swap_watchdog()
proc = subprocess.Popen(
[ps_exe, "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-File", str(ps1)],
creationflags=subprocess.DETACHED_PROCESS
| subprocess.CREATE_NEW_PROCESS_GROUP
| getattr(subprocess, "CREATE_NO_WINDOW", 0),
)
Síntoma:
- El watchdog PID se registra en
update_watchdog.log(“watchdog lanzado (pid 20072)”). - Pero el script PowerShell jamás escribe su primera línea.
- El vigilante muere silenciosamente; el Agente permanece muerto si el AV bloqueó el arranque.
- Observado en incidentes 03-07 y 05-07-2026 (recovery manual necesario).
Después (Agent 2.13.3+)
# updater.py:_launch_swap_watchdog()
proc = subprocess.Popen(
[ps_exe, "-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-File", str(ps1)],
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | getattr(subprocess, "CREATE_NO_WINDOW", 0),
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
close_fds=True,
)
Cambios:
- ❌ Quitar:
subprocess.DETACHED_PROCESS(anulaCREATE_NO_WINDOW— modos excluyentes). - ✅ Mantener:
CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW(consola propia, oculta). - ✅ Añadir:
stdin/stdout/stderr=DEVNULL(PowerShell obtiene handles válidos).
Comportamiento:
- El script PowerShell inicia correctamente.
- El proceso sobrevive al padre (Windows permite hijos sin padre).
- El vigilante monitorea
/health, relanza si es necesario, revierte si todo falla.
Verificación
Diagnóstico reproducido en laboratorio
El commit incluye una sección en terminal/agent/version.py documentando la reproducción del problema fuera del Agente:
- Mismo setup de flags (DETACHED_PROCESS + CREATE_NO_WINDOW) → PowerShell muere.
- Nuevo setup (sin DETACHED_PROCESS, + DEVNULL) → PowerShell corre entero aunque el padre haga
os._exit(0)al instante.
Test de regresión
Archivo: tests/agent/test_agent_updater_trace.py::test_watchdog_launch_sin_detached_process
- Inspecciona el source de
_launch_swap_watchdog. - Verifica ausencia de
DETACHED_PROCESS. - Verifica presencia de
CREATE_NO_WINDOWyDEVNULL. - Nota: El test es por inspección (CI Linux no tiene flags de Windows).
Impacto en la flota
| Versión | Watchdog | Auto-update fallido → Resultado |
|---|---|---|
| ≤2.13.2 | ❌ Mudo (muere silenciosamente) | AV bloquea → Agente muerto hasta relanzar a mano |
| 2.13.3+ | ✅ Vivo y funcional | AV bloquea → Watchdog relanza o revierte automáticamente |
⚠️ Transición hacia 2.13.3
- El updater aún es viejo → watchdog sigue mudo durante ese swap.
- Si el AV bloquea 2.13.3, es necesario un relanzamiento manual una última vez.
- Desde 2.13.3 en adelante, todos los swaps posteriores usan el nuevo vigilante.
Detalles técnicos
¿Por qué DETACHED_PROCESS mata el watchdog?
En Windows, subprocess.Popen(creationflags=...) acepta un conjunto de flags de CreateProcessW:
DETACHED_PROCESS(0x00000008): el nuevo proceso se desacopla del proceso padre (sin consola heredada).CREATE_NO_WINDOW(0x08000000): el nuevo proceso abre una ventana de consola privada pero la oculta.
Son mutuamente excluyentes en la práctica: si usas ambos, Windows prioriza DETACHED_PROCESS e ignora CREATE_NO_WINDOW. El resultado es un proceso sin consola válida.
Cuando Python no proporciona handles explícitos (stdin/stdout/stderr):
DETACHED_PROCESSdeja los handles sin inicializar.- PowerShell.exe en su host initialization necesita escribir en la consola estándar.
- Sin handles válidos,
SetConsoleModefalla → PowerShell aborta antes de cargar el script.
¿Por qué funciona con CREATE_NO_WINDOW + DEVNULL?
CREATE_NO_WINDOWsinDETACHED_PROCESSabre una consola invisible (válida internamente).stdin=DEVNULL, stdout=DEVNULL, stderr=DEVNULLproporciona handles de dispositivo nulo (siempre válidos).- PowerShell inicializa correctamente; los hijos en Windows permanecen vivos aunque el padre haga
os._exit(0).
Código afectado
| Archivo | Líneas | Cambio |
|---|---|---|
terminal/agent/core/updater.py | 249–269 | Flags de subprocess.Popen en _launch_swap_watchdog() |
terminal/agent/version.py | 11–23 | Documentación de causa raíz en comentarios |
tests/agent/test_agent_updater_trace.py | 53–69 | Test de regresión (src inspection) |
config/settings/base.py | 10 | APP_VERSION 1.45.8 → 1.45.9 |
CHANGELOG.md | 9–15 | Entrada de release |
RELEASE_NOTES.md | 12–22 | Nota técnica para usuarios |
Cimiento arquitectónico
Esta es una corrección de bug crítico del cimiento agent-robustez F2 (introducido en 2.11.4):
El watchdog post-swap monitorea que la versión nueva responde (
/health); si el AV la mató al arrancar, la relanza; si tampoco arranca, restaura la versión anterior — el auto-update ya no puede dejar un Agente muerto.
Hasta hoy, el watchdog no corría de verdad por el conflicto de flags. Ahora sí.
Véase también
- [[incident—20260705—watchdog-auto-update-mudo]]
- [[decision—20260705—flags-createprocess-windows-watchdog]]
- [[entity—terminal—service—updater-swap-watchdog]]
- [[concept—infra—auto-update-resilience]]
- [[runbook—terminal—diagnose-agent-update-failure]]