Compare commits

..
2 Commits
Author SHA1 Message Date
devops d9c2037c59 Merge branch 'docs/incidente-tmux-host-2026-09-25'
Build and Push Docs Portal / Construir y publicar Docs Portal (push) Successful in 14m51s
Build and Push Docs Portal / Escaneo de Secretos (Gitleaks) (push) Successful in 1m15s
2026-09-26 12:39:31 +00:00
devopsandClaude Opus 5.5 7549dc86d2 docs(playbooks): incidente cadena de 4 fallos en tmux-host (2026-09-25/26)
Documenta la cadena de causas encontradas tras eliminar tmux-host por
"no toma las sesiones remotas": volumenes de auth faltantes al recrear,
set -e matando al supervisor del healthcheck, IPv6 roto en el NAS
(timeout a Anthropic) y, como causa original, el token OAuth vencido.
Agrega la fila de expiracion OAuth a "Que sobrevive a que" en
sesion-tmux-persistente.md con enlace cruzado al playbook.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018r19s4H8MBGegqToqw5guP
2026-09-26 12:39:30 +00:00
3 changed files with 333 additions and 0 deletions

No files matched your search

@@ -408,6 +408,14 @@ después del swap.
| `docker restart tmux-host` (reinicio del NAS) | **No al toque, pero se auto-recupera solo** — el proceso `tmux server` muere con el contenedor; `healthcheck.sh` la recrea en el próximo ciclo (máx. 30 min) sin intervención, o `lab-session.sh` la crea al instante si alguien se reconecta antes | Sí, vía el volumen montado en `/root/.claude` | Sí, desde el 2026-09-20 — `--continue` retoma la conversación más reciente en `WORKDIR` (ver gotcha arriba) |
| `docker rm` + recrear el contenedor desde cero | No, pero igual se auto-recupera sola (mismo mecanismo de arriba) | Sí, vía el mismo volumen — no hace falta volver a autenticar ni reconfirmar el diálogo de confianza del folder (ver el mount de `claude-code-home.json`) | Sí, mismo mecanismo — validado en vivo durante el swap de imagen del 2026-09-20 |
| Falla de red real del host/NAS | N/A | N/A — `healthcheck.sh` la detecta y notifica, pero no la corrige (fuera de su alcance) | N/A |
| Expiración del token OAuth de Claude Code | Sí — el proceso `claude` sigue vivo en el pane, pero muestra `Login expired` y Remote Control queda desconectado (`OAuth token unavailable`) | **No** — requiere `/login` manual (una vez alcanza: el volumen de credenciales es compartido) y después `/remote-control` en **cada** sesión ya abierta. Recrear el contenedor **no** lo arregla | Sí, una vez reautenticado — misma sesión, mismo proceso |
!!! warning "Incidente relacionado"
La fila de expiración del token OAuth salió del
[incidente de la cadena de 4 fallos en `tmux-host` (2026-09-25/26)](../playbooks/incidente-tmux-host-cadena-fallos-2026-09-25.md).
El síntoma "no aparecen las sesiones remotas" llevó a eliminar el
contenedor, aunque la causa real era un token vencido. **Antes de
tocar el contenedor, mirá el pane de cada sesión.**
## Validación en vivo
@@ -0,0 +1,324 @@
# Incidente: cadena de 4 fallos en `tmux-host` (2026-09-25/26)
| | |
|---|---|
| **Ventana del incidente** | Noche del 2026-09-25 al 26 (hora local); en `health.log`, de 2026-09-26 04:11Z a ~06:20Z |
| **Servicio afectado** | Contenedor `tmux-host`: las sesiones `lab` y `cloud-practice` de Claude Code y su Remote Control desde claude.ai (ver [Sesión tmux persistente](../arquitectura/sesion-tmux-persistente.md)) |
| **Impacto** | Las sesiones remotas no aparecían en claude.ai. Mientras se intentaba arreglar, se perdió el login y la confianza del folder, el supervisor del healthcheck quedó muerto y `claude` no podía conectarse a Anthropic |
| **Disparador** | Se eliminó el contenedor `tmux-host` a mano creyendo que era la causa |
## Contexto / disparador
El síntoma inicial era uno solo: **"no toma las sesiones remotas"**. Las
sesiones de Claude Code dentro de `tmux-host` no aparecían en claude.ai.
Parecía un problema del contenedor, así que se eliminó `tmux-host` para
recrearlo desde cero.
En realidad ese síntoma tenía una causa que no tenía nada que ver con el
contenedor: el token OAuth estaba vencido (causa **d**, más abajo). Al
recrear el contenedor, la mitad de la noche se fue en resolver tres fallos
reales que *aparecieron* en el camino (causas **a**, **b** y **c**). Cada
uno se encontró recién después de resolver el anterior. Recién al final
apareció la causa original.
!!! info "Fuentes de la evidencia"
Este playbook se escribió desde dentro del `tmux-host` ya recuperado.
Los datos marcados **(verificado)** se confirmaron contra el estado
real del sistema al escribirlo: `docker inspect`, `docker images`,
`health.log`, `sysctl`, `curl`, `git diff` y los panes de tmux. Los
marcados **(reportado)** vienen del relato de la sesión de
recuperación. Esa sesión se hizo desde fuera del contenedor y no dejó
transcript ni historial de shell que se pueda revisar.
---
## Causa a — Contenedor recreado sin los volúmenes de autenticación
**Síntoma.** Al recrear `tmux-host`, `claude` volvía a pedir login y el
diálogo de confianza del folder. `--continue` no encontraba ninguna
conversación previa y arrancaba una en blanco.
**Diagnóstico.** El `docker run` que se usó para recrear el contenedor
se armó de memoria. Tenía solo 3 de los 5 volúmenes documentados en
[Sesión tmux persistente](../arquitectura/sesion-tmux-persistente.md)
**(reportado)**. Faltaban justamente los dos de autenticación y estado
de Claude Code:
| Volumen faltante | Qué se perdía |
|---|---|
| `/DATA/AppData/k3d-lab-v3/claude-code-home` → `/root/.claude` | Credenciales OAuth, historial de conversaciones (por eso `--continue` no encontraba nada), plugins y memoria |
| `/DATA/AppData/k3d-lab-v3/claude-code-home.json` → `/root/.claude.json` | Confianza del folder y el flag de Remote Control |
Sin esos mounts, `/root/.claude*` vive en el filesystem efímero del
contenedor, así que cada recreación arrancaba "de fábrica". El contenedor
se reconstruyó varias veces con el mismo comando incompleto antes de
comparar contra el doc de arquitectura **(reportado)**. `health.log`
muestra las sesiones recreándose desde cero a las 04:11Z, 04:40Z, 04:49Z,
05:03Z, 05:18Z, 06:08Z y 06:10Z **(verificado)**.
**Fix.** Recrear el contenedor con el `docker run` completo de la
sección *Contenedor: `tmux-host`* del doc de arquitectura. Los 5 volúmenes
van juntos, sin excepciones.
**Validación en vivo (verificado).** `docker inspect tmux-host` sobre el
contenedor final (creado `2026-09-26T06:10:55Z`, imagen
`tmux-host:2026-09-26-fix-real`) lista los 5 mounts:
```
/var/run/docker.sock -> /var/run/docker.sock
/DATA/AppData/k3d-lab-v3/claude-code-home -> /root/.claude
/DATA/AppData/k3d-lab-v3/claude-code-home.json -> /root/.claude.json
/DATA -> /DATA
/media/HDD-Storage -> /media/HDD-Storage
```
La sesión `lab` retomó la conversación anterior con `--continue`, sin
pedir de nuevo la confianza del folder.
!!! warning "Lección"
**Antes de improvisar un `docker run` de memoria, leer siempre el doc
de arquitectura que ya existe.** El comando completo y el motivo de
cada volumen ya estaban documentados en `sesion-tmux-persistente.md`.
El tiempo perdido reconstruyendo con un comando incompleto salió de no
abrirlo primero.
---
## Causa b — `set -e` heredado mataba al supervisor del healthcheck
**Síntoma.** `healthcheck.sh` corre en background y no tenía quién lo
relanzara si moría. Para cubrir eso se agregó a `entrypoint.sh` un
supervisor: un loop que lo relanza tras 10 s. Aun así, cuando
`healthcheck.sh` terminaba con error, no se relanzaba y el contenedor
seguía vivo sin healthcheck.
**Diagnóstico.** `entrypoint.sh` tenía `set -eu` desde antes. El
supervisor corre como función en un subshell (`supervise_healthcheck &`)
y hereda el `-e`. Cuando `healthcheck.sh` devolvía un código distinto de
cero, `set -e` mataba al propio subshell del supervisor *en esa misma
línea*, antes de llegar al `echo WARN` y al `sleep 10` que lo relanzaban.
Es decir, el supervisor moría justo en el único caso para el que existía.
**Paso en falso: el fix que nunca llegó al disco.** La primera
"corrección" (`set -eu` → `set -u`) nunca se guardó en el archivo, y la
imagen se reconstruyó dos veces con el contenido viejo **(reportado)**.
La evidencia sigue en el host **(verificado)**: las etiquetas
`tmux-host:2026-09-25-supervised` y `tmux-host:2026-09-25-supervised-v2`
apuntan a la **misma imagen, `ad7f6eff9537`**. Docker reutilizó todas las
capas porque el archivo no había cambiado. Recién después de confirmar
con `cat entrypoint.sh` que el cambio no estaba, se guardó de verdad y se
construyó `tmux-host:2026-09-26-fix-real` (`7e4c94bf0bc8`, 05:03Z).
**Fix.** En `scripts/tmux-host/entrypoint.sh`:
```diff
-set -eu
+set -u
-/usr/local/bin/healthcheck.sh &
+supervise_healthcheck() {
+ while true; do
+ /usr/local/bin/healthcheck.sh
+ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) WARN: healthcheck.sh salio inesperadamente (exit=$?) -- relanzando en 10s" \
+ >> /DATA/AppData/k3d-lab-v3/tmux-host/logs/health.log 2>/dev/null || true
+ sleep 10
+ done
+}
+supervise_healthcheck &
exec tail -f /dev/null
```
**Validación en vivo (verificado).** `health.log` registra el supervisor
actuando en un caso real. `healthcheck.sh` murió por SIGKILL, se registró
el aviso y se relanzó solo 10 segundos después:
```
2026-09-26T05:04:17Z WARN: healthcheck.sh salio inesperadamente (exit=137) -- relanzando en 10s
[2026-09-26T05:04:27Z] === healthcheck inicio (intervalo=1800s) ===
```
!!! warning "Lección"
**Verificar el archivo en disco (`cat`, `git diff`) ANTES de
reconstruir la imagen.** No hay que asumir que el editor guardó el
cambio. Dos etiquetas de imagen con el mismo ID indican que el build
no tuvo cambios.
---
## Causa c — IPv6 roto en el NAS: timeout de conexión a Anthropic
**Síntoma.** Con el contenedor ya bien montado, `claude` fallaba al
arrancar con `Unable to connect to Anthropic services` a los ~10 s.
**Diagnóstico** con `curl -v https://api.anthropic.com/` **(reportado)**:
1. El resolver devolvía primero la dirección IPv6 de `api.anthropic.com`.
2. La ruta IPv6 del NAS no sale a internet: el intento fallaba al
instante con `Network unreachable`.
3. El fallback automático a IPv4 terminaba funcionando, pero la conexión
completa tardaba **~14 s**. Eso es más que el timeout interno de
Claude Code (~10 s), así que el CLI se rendía antes de que el
fallback llegara a conectar.
**Primer intento: parche parcial.**
`NODE_OPTIONS=--dns-result-order=ipv4first` le indica a Node que prefiera
IPv4 al resolver. Ayudó, pero no resultó confiable en todos los casos
**(reportado)**, y además solo corregía el proceso de Node, no la red del
host. Se descartó y no quedó en la imagen: el contenedor final no tiene
`NODE_OPTIONS` en su entorno **(verificado)**.
**Fix de raíz.** Deshabilitar IPv6 a nivel de sistema operativo del NAS,
en caliente y persistente:
```bash
# En caliente
sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
# Persistente: /etc/sysctl.conf del host
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
```
Esto se tiene que hacer **en el host, no dentro del contenedor**.
`tmux-host` corre con `--network host`, así que comparte el network
namespace del NAS. Docker no permite fijar sysctls de red
(`--sysctl net.*`) en un contenedor con `--network host`, porque
cambiarían la red de todo el host.
**Validación en vivo (verificado, 2026-09-26 ~06:40Z):**
```
$ sysctl net.ipv6.conf.all.disable_ipv6 net.ipv6.conf.default.disable_ipv6
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
$ getent ahosts api.anthropic.com
160.79.104.10 STREAM api.anthropic.com # solo IPv4, sin AAAA
$ curl -w 'connect=%{time_connect}s total=%{time_total}s' https://api.anthropic.com/
ip=160.79.104.10 connect=0.035s total=0.65s # antes: ~14 s
```
`/etc/sysctl.conf` en el host contiene las dos líneas (escrito
2026-09-26 05:55:53Z). En ZimaOS, `/` es un `squashfs` de solo lectura,
pero `/etc` es un overlay con capa de escritura en
`/mnt/overlay/upper_etc` (ext4 en `/dev/sdc7`), así que el archivo
persiste.
!!! note "Pendiente de validar"
Todavía no se probó que el sysctl persistente se aplique después de un
**reinicio real del NAS**. Tampoco se sabe si una actualización de
ZimaOS pisa `upper_etc`. En el próximo reinicio, revisar
`sysctl net.ipv6.conf.all.disable_ipv6` antes de dar por cerrado este
punto.
---
## Causa d — Token OAuth vencido: Remote Control desconectado sin recuperación automática
**Síntoma.** Con todo lo anterior resuelto, las sesiones remotas
*seguían* sin aparecer en claude.ai. Este era el síntoma original de la
noche.
**Diagnóstico (verificado).** Ambos procesos `claude --remote-control
--continue` estaban corriendo (`ps`), y el healthcheck había recreado
las dos sesiones sin problema (`health.log`, 06:10:58Z). Así que el
auto-arranque funcionaba. El contenido de los panes de tmux mostró la
causa:
- Sesión `lab`: `Login expired · Please run /login`
- Sesión `cloud-practice`:
`Remote Control disconnected — OAuth token unavailable — run /login to restore Remote Control`
El token OAuth de Claude Code había vencido. No había ningún problema de
red, de contenedor ni de volúmenes. **Esta causa no tenía relación con
ninguna de las tres anteriores.** Eliminar el contenedor nunca podría
haberla resuelto.
**Fix.**
1. `/login` en **una** sesión. Las credenciales viven en
`/root/.claude/.credentials.json`, que es el volumen compartido por
todas las sesiones del contenedor.
2. `/remote-control` → **1. Enable Remote Control** en **cada** sesión ya
abierta. Un proceso `claude` que ya estaba corriendo **no** se
reconecta solo al renovarse el token. Hay que reactivarlo en cada uno.
Para una sesión a la que no estás adjunto:
```bash
tmux send-keys -t cloud-practice '/remote-control' Enter
# aparece el menú con "1. Enable Remote Control" ya seleccionado
tmux send-keys -t cloud-practice Enter
```
**Validación en vivo (verificado).** El pane de `cloud-practice` pasó a
mostrar `/remote-control is active · Continue here, on your phone, or at
https://claude.ai/code/session_…`, y en `lab` se activó con
`/remote-control` después del `/login`. Las dos sesiones volvieron a
aparecer en claude.ai.
!!! tip "Diferencia clave con las otras causas"
El healthcheck **sí** recrea sola una sesión tmux caída, y `--continue`
retoma la conversación. Un **token vencido no se recupera solo**: hace
falta que una persona se reautentique (`/login`) y reactive Remote
Control (`/remote-control`) en cada sesión abierta. Recrear el
contenedor no lo arregla. Con los volúmenes bien montados, el
contenedor nuevo lee el mismo token vencido.
---
## Cronología resumida
Horas en UTC, según `health.log`, `docker images` y los timestamps de
archivos cuando existen.
| Hora (UTC) | Qué pasó | ¿Resolvió el síntoma original? |
|---|---|---|
| antes de 04:11 | Remote Control se desconecta porque vence el token OAuth (causa **d**, todavía sin identificar) | — |
| ~04:10 | Se elimina `tmux-host` creyendo que es la causa | No |
| 04:11 → 05:18 | Varias recreaciones con un `docker run` de memoria, sin los mounts de auth: se pide login y confianza de nuevo y `--continue` arranca vacío (causa **a**) | No, y además empeoró |
| 04:39 | Se agrega el supervisor del healthcheck; builds `supervised` y `supervised-v2`, **misma imagen `ad7f6eff9537`** (el fix de `set -e` no estaba guardado) | No |
| 05:03 | Se confirma con `cat`, se guarda de verdad y se construye `fix-real` (`7e4c94bf0bc8`) con `set -u` (causa **b**) | No |
| 05:04 | El supervisor relanza `healthcheck.sh` tras un `exit=137`: primera prueba real | No |
| hasta ~05:55 | `claude` falla con `Unable to connect to Anthropic services`; con `curl -v` se ve IPv6 unreachable y ~14 s de fallback; se prueba `NODE_OPTIONS` (parcial) | No |
| 05:55 | Se deshabilita IPv6 en el host, en caliente y en `/etc/sysctl.conf` (causa **c**) | No |
| 06:10 | Contenedor final: `fix-real` + los 5 volúmenes, recreado según el doc de arquitectura (causa **a** cerrada) | No: Remote Control sigue desconectado |
| ~06:20 | Los panes muestran `Login expired` / `OAuth token unavailable` → `/login` + `/remote-control` en cada sesión (causa **d**) | **Sí** |
**Camino corto para la próxima vez.** Si "no aparecen las sesiones
remotas", **antes de tocar el contenedor** mirar el pane de cada sesión:
```bash
for s in $(tmux ls -F '#S'); do echo "== $s"; tmux capture-pane -p -t "$s" | grep -v '^\s*$' | tail -5; done
```
Si aparece `Login expired` u `OAuth token unavailable`, la solución es
`/login` + `/remote-control`. No hace falta nada más.
## Lección general
Un solo síntoma ("no toma las sesiones remotas") puede tener **varias
causas independientes superpuestas**. En este caso, tres de ellas las
provocó el propio intento de arreglo. Que la primera causa real
encontrada se haya resuelto no significa que era *la* causa del síntoma.
Lo que destrabó el incidente fue seguir verificando con evidencia
concreta después de cada fix, en vez de asumir que "ya quedó": el
contenido de los panes, `health.log`, `docker inspect`, `docker images`
(IDs repetidos) y `curl -v`. Cada una de las cuatro causas se confirmó
con un dato observable, no por deducción.
## Pendientes
- [ ] **Commitear el fix de `entrypoint.sh`.** La imagen `fix-real` que
corre en producción lo tiene, pero en el repo `scripts` el cambio
sigue como modificación local sin commitear (`git status` →
`M tmux-host/entrypoint.sh`). Si alguien reconstruye la imagen
desde `main`, vuelve el `set -eu` sin supervisor.
- [ ] Validar que IPv6 siga deshabilitado después del próximo reinicio
real del NAS (ver la nota de la causa c).
- [ ] Limpiar las etiquetas de imagen viejas de `tmux-host`
(`supervised`, `supervised-v2`, `local`, `test-fix`, etc.) para no
confundir cuál es la vigente.
+1
View File
@@ -114,6 +114,7 @@ nav:
- Realm HTTP y Cosign: playbooks/incidente-cosign-realm-http-tunnel-2026-08.md
- Contención de recursos Gitea/runner: playbooks/incidente-contencion-recursos-gitea-runner-2026-08.md
- Blackhole MTU cross-node: playbooks/incidente-mtu-blackhole-cross-node-2026-08.md
- "Cadena de fallos tmux-host": playbooks/incidente-tmux-host-cadena-fallos-2026-09-25.md
- "Migración a platform-templates": playbooks/migracion-platform-templates-2026-08.md
- Aprendizajes:
- Notas sueltas: aprendizajes/notas-sueltas.md