CreaRack-SL

Dos bugs críticos del build 2.14.0 en bootstrap e updater (07-07-2026)

Resumen

Durante la validación F2 del build 2.14.0, se detectaron dos bugs críticos en el Lanzador (bootstrap) del Agente:

  1. Bug bootstrap: ModuleNotFoundError: No module named 'app_main' en el .exe
  2. Bug updater: Race condition en _apply_or_defer() deja swap diferido colgado para siempre

Ambos fueron cazados en vivo y reparados. El build se retiró; 2.13.6 restaurado como fallback.


Bug 1: FrozenImporter bloquea importación de app_main.py

Síntoma

ModuleNotFoundError: No module named 'app_main'

en el .exe compilado con PyInstaller (no repite en dev).

Causa raíz

PyInstaller instala FrozenImporter en sys.meta_path con prioridad máxima sobre los finders de sys.path. El archivo app_main.py viaja como --add-data (data cruda en agent_modules/), no como módulo congelado, así que:

  1. FrozenImporter lo busca en su índice congelado → NO LO ENCUENTRA
  2. Los finders normales nunca se ejecutan → resolución falla
  3. Error explosivo al startup

Fix: importación por ruta explícita

Función nueva _import_app_main(source_dir) en terminal/agent/main.py:

  • Carga app_main.py usando importlib.util.spec_from_file_location()
  • Registra el módulo en sys.modules["app_main"] → las importaciones posteriores (from app_main import ... en routes/) resuelven al mismo módulo
  • Si el fichero está ausente → ModuleNotFoundError con traza clara (“empaquetado incompleto”)
  • Si ya está en sys.modules → devuelve el existente (reutilización en dev/tests)

Código: terminal/agent/main.py#L165–L187

Tests añadidos

- test_carga_por_ruta_y_registra_en_sys_modules()  # módulo cargado e indexado
- test_ya_en_sys_modules_no_recarga()               # caché + no duplicación
- test_fichero_ausente_error_claro_con_traza()     # error = bootstrap fallido

Bug 2: Swap diferido colgado en _apply_or_defer()

Síntoma

Build 2.14.0 quedó en estado “swap diferido” indefinidamente (casually detectado por Edu a las 14:23 del 07-07). El Agente no falló, pero el actualizador nunca terminó.

Causa raíz

En terminal/agent/core/updater.py, función _apply_or_defer():

if _is_idle():
    _apply_swap()
    return  # ← INCOMPLETO
# esperando...
while True:
    await asyncio.sleep(_IDLE_RETRY_SECS)
    if _is_idle():
        # reintenta...

Escenario de fallo:

  1. Agente está en reposo (preparándose para el sueño de 240s)
  2. _apply_or_defer() detecta que NO está “verdaderamente ocioso” (sesiones aún activas, estado transitorio)
  3. Aplica swap pero descarta/difiere → NEW_EXE persiste en disco
  4. Retorna (línea 446)
  5. El bucle while True nunca corre porque ya pasó la rama if _is_idle()
  6. Agente entra en sueño, deja NEW_EXE huérfano

Nadie limpia el staged; next boot asume aplicación pendiente pero nada ocurre.

Fix: fall-through al bucle idle

if _is_idle():
    _apply_swap()
    if not NEW_EXE.exists():
        return  # aplicado por otra vía o descartado
    # Si llegamos aquí: swap diferido con Agente ocioso.
    # Sin este fall-through el bucle de abajo no se alcanzaba.

logger.info("[Updater] Update preparado; esperando...")
while True:
    # Garantizado que se ejecuta si el swap se diferó

Lógica mejorada:

  • Si _apply_swap() tiene éxito → NEW_EXE desaparece (os._exit o reboot) → early return
  • Si NEW_EXE aún existe DESPUÉS de _apply_swap() → fue diferido → continúa a bucle idle
  • El bucle idle reintenta cada _IDLE_RETRY_SECS hasta aplicarse

Comentario explicativo en el código

# Diferido con el Agente OCIOSO (reposo aún madurando) → sin este fall-through
# el staged quedaba esperando PARA SIEMPRE hasta el próximo arranque (cazado
# en vivo el 07-07-2026: el 2.14.0 de Edu se quedó en "swap diferido" a las
# 14:23 y nadie lo recogió...)

Impacto: El build retirado no tenía este control; 2.13.6 sí lo tenía.


Validación y rollback

Fecha/HoraEvento
07-07 14:00Build 2.14.0 emitido y desplegado a sidecar de validación F2
07-07 14:23Edu reporta: swap diferido, Agente aparentemente sano, pero actualización colgada
07-07 ~14:30Análisis: doble bug identificado (bootstrap + updater)
07-07 ~14:45Build 2.14.0 retirado del server (sidecar rollback a 2.13.6)
DespuésFixes implementados; tests añadidos; recompilación para revalidación F2

Tests de regresión

Nuevos en tests/agent/test_agent_launcher.py:

class TestImportAppMain:
    """Regresión del bug del primer build 2.14.0"""
    
    def test_carga_por_ruta_y_registra_en_sys_modules()
        # Verifica: fichero → módulo en sys.modules
    
    def test_ya_en_sys_modules_no_recarga()
        # Verifica: segunda invocación no recarga (caché)
    
    def test_fichero_ausente_error_claro_con_traza()
        # Verifica: falta de archivo = excepción clara + log

Lecciones aprendidas

  1. PyInstaller + --add-data: Los ficheros data NO se congelan; necesitan carga explícita si deben importarse como módulos.
  2. Race conditions en bucles de sueño: Verificar que el bucle retry se alcanza incluso si la rama de “ocioso” no aplica directamente.
  3. Validación F2 crítica: Los bugs de empaquetado binario NO aparecen en dev. Validar con el .exe real antes de release.

Véase también

  • [[entity—terminal—service—updater-swap-watchdog]]