Agrega tres gotchas nuevos a sesion-tmux-persistente.md, mismo estilo que los existentes: - Remote Control nunca se activaba solo: faltaba el flag --remote-control en launch_claude_in_session(). - Cada recreacion empezaba una conversacion en blanco: se agrego --continue al comando de lanzamiento. - Actualizar tmux-host en caliente mata al proceso que hace el swap (corre dentro del propio contenedor): documentado el workaround de contenedor sibling via docker.sock. Tambien actualiza la tabla "Que sobrevive a que" con una columna de continuidad de conversacion, y el comando citado en la seccion de healthcheck automatico. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QMP7ULbxbsB5g1vELNAT3D
432 lines
23 KiB
Markdown
432 lines
23 KiB
Markdown
# Sesión persistente de Claude Code (tmux en Docker)
|
|
|
|
## Problema que resuelve
|
|
|
|
Correr Claude Code directo en una shell SSH sobre el NAS lo ata al
|
|
ciclo de vida de esa conexión: un corte de red, cambiar de laptop a
|
|
tablet, o cerrar la terminal por accidente mata el proceso y con él
|
|
todo el contexto de trabajo en curso — no hay forma de "retomar
|
|
donde quedó" porque el proceso ya no existe.
|
|
|
|
**Esto es un problema distinto al de los límites de uso/cuota de
|
|
Claude Code.** Este setup no aumenta cuota de mensajes ni tokens —
|
|
eso lo controla el backend de Anthropic sin importar dónde corra el
|
|
CLI. Lo único que resuelve es que la *sesión de terminal* sobreviva
|
|
a cortes de red o cambios de dispositivo; si la cuota se agota, sigue
|
|
agotada exactamente igual dentro de este setup.
|
|
|
|
## Por qué Docker y no instalación nativa
|
|
|
|
ZimaOS no tiene gestor de paquetes tradicional utilizable para esto:
|
|
`/` está montado como `squashfs` de solo lectura (imagen del SO
|
|
inmutable), y `/usr` es un overlay de solo lectura sobre eso —
|
|
`/usr/local/bin` ni siquiera existe y no se puede crear, tampoco como
|
|
root (`mkdir` falla con "Read-only file system"). No hay dónde
|
|
instalar Node.js, npm ni tmux directamente sobre el host.
|
|
|
|
Un contenedor Docker sortea esto por completo: tiene su propio
|
|
filesystem de escritura (overlay2), independiente del `squashfs` del
|
|
host — ahí es donde `npm install -g` deja el binario de `claude` sin
|
|
tocar el host en absoluto. Lo único que el contenedor toma del host
|
|
es lo que se monta explícitamente como volumen.
|
|
|
|
## Arquitectura
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
SSH["Cliente SSH<br/>(laptop / tablet / lo que sea)"] -->|docker exec| C["Contenedor tmux-host<br/>--network host, --restart unless-stopped"]
|
|
C --> T["tmux server<br/>sesión 'lab'"]
|
|
T --> CC["proceso claude<br/>(Claude Code)"]
|
|
C --> HC["healthcheck.sh (background)<br/>cada 30 min"]
|
|
HC -.->|curl| NET["gitea.cruzcloud.net"]
|
|
HC -.->|recrea si falta| T
|
|
C -.->|bind mount| DATA["/DATA (host)<br/>repos y configs del lab"]
|
|
C -.->|bind mount| SOCK["/var/run/docker.sock (host)<br/>control de Docker/k3d"]
|
|
C -.->|bind mount| HOME["/DATA/AppData/k3d-lab-v3/claude-code-home (host)<br/>-> /root/.claude"]
|
|
```
|
|
|
|
El contenedor sigue vivo aunque la conexión SSH se corte — es el
|
|
servidor tmux, no la shell del cliente, el que sostiene el proceso de
|
|
Claude Code. `--restart unless-stopped` además lo revive si el NAS
|
|
reinicia.
|
|
|
|
## Contenedor: `tmux-host`
|
|
|
|
La imagen se construye desde `scripts/tmux-host/Dockerfile` (antes era
|
|
un `docker run ... alpine sh -c "apk add ..."` inline — mismo contenido,
|
|
pero ahora como imagen versionada en vez de reinstalar todo en cada
|
|
arranque). El Dockerfile fija `WORKDIR /DATA/AppData/k3d-lab-v3/lab-cruzcloud`
|
|
(la raíz del lab, con `CLAUDE.md`, `apps-registry/`, `platform-infra/` y
|
|
`scripts/`) y agrega `healthcheck.sh` (ver más abajo) como proceso en
|
|
background del contenedor.
|
|
|
|
```bash
|
|
docker build -t tmux-host:local scripts/tmux-host
|
|
|
|
docker run -d \
|
|
--name tmux-host \
|
|
--restart unless-stopped \
|
|
-v /DATA:/DATA \
|
|
-v /media/HDD-Storage:/media/HDD-Storage \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
-v /DATA/AppData/k3d-lab-v3/claude-code-home:/root/.claude \
|
|
-v /DATA/AppData/k3d-lab-v3/claude-code-home.json:/root/.claude.json \
|
|
--network host \
|
|
tmux-host:local
|
|
```
|
|
|
|
`WORKDIR` hace que `docker exec` (y por lo tanto `lab-session.sh`)
|
|
arranquen ya posicionados en la raíz del lab sin tener que navegar a
|
|
mano — `lab-session.sh` además pasa la ruta explícita vía `tmux new -c`
|
|
por las dudas, para no depender de que `docker exec` herede el `WORKDIR`
|
|
de la imagen.
|
|
|
|
| Volumen | Destino | Por qué |
|
|
|---|---|---|
|
|
| `/DATA` | `/DATA` | Acceso a los repos (`apps-registry`, `platform-infra`, `scripts`) y configs del lab, mismos paths que en el host |
|
|
| `/media/HDD-Storage` | `/media/HDD-Storage` | Ver "Gotcha: el symlink `/DATA/AppData`" abajo — sin esto, los repos no se ven dentro del contenedor pese a montar `/DATA` |
|
|
| `/var/run/docker.sock` | `/var/run/docker.sock` | Seguir controlando Docker y k3d (contenedores, `k3d node`, etc.) desde dentro del contenedor |
|
|
| `/DATA/AppData/k3d-lab-v3/claude-code-home` | `/root/.claude` | Persistir login/config de Claude Code — sobrevive no solo a un `docker restart`, sino a recrear el contenedor entero |
|
|
| `/DATA/AppData/k3d-lab-v3/claude-code-home.json` | `/root/.claude.json` | Ver "Gotcha: la confianza del folder no persistía" abajo — es un archivo suelto, no vive dentro de `~/.claude/` |
|
|
|
|
## Gotcha: el symlink `/DATA/AppData`
|
|
|
|
Montar `-v /DATA:/DATA` **no alcanza** para ver los repos dentro del
|
|
contenedor. `/DATA/AppData` en el host es un symlink *absoluto*:
|
|
|
|
```
|
|
/DATA/AppData -> /media/HDD-Storage/AppData
|
|
```
|
|
|
|
Los repos del lab (`lab-cruzcloud`) viven físicamente en el disco
|
|
btrfs `/media/HDD-Storage`, no en el ext4 de `/DATA` — ZimaOS lo
|
|
expone dentro de `/DATA` como submount (`/DATA/.media/HDD-Storage/...`,
|
|
que sí viaja al contenedor porque Docker hace bind recursivo por
|
|
defecto) pero el path "bonito" `/DATA/AppData/...` que usan `CLAUDE.md`
|
|
y todos los scripts del lab llega ahí vía ese symlink absoluto, no vía
|
|
el submount. Un symlink absoluto que apunta fuera del árbol montado
|
|
queda roto dentro del contenedor aunque el submount subyacente esté
|
|
perfectamente accesible por otra ruta.
|
|
|
|
**Cómo se detectó:** todo el trabajo de configuración inicial de este
|
|
mismo setup se hizo con `git`/`docker exec` corriendo *en el host*, no
|
|
dentro del contenedor — nadie había probado `cd
|
|
/DATA/AppData/k3d-lab-v3/lab-cruzcloud` desde una sesión `lab` real
|
|
hasta que se preguntó explícitamente por esto. `ls` devolvía "No such
|
|
file or directory" pese a que el volumen de `/DATA` estaba montado y
|
|
"andando".
|
|
|
|
**Fix:** montar también `-v /media/HDD-Storage:/media/HDD-Storage`,
|
|
para que el symlink resuelva exactamente igual que en el host.
|
|
|
|
## Gotcha: git "dubious ownership" dentro del contenedor
|
|
|
|
Con el mount anterior ya arreglado, `git status`/`git log` dentro del
|
|
contenedor fallan con:
|
|
|
|
```
|
|
fatal: detected dubious ownership in repository at '/media/HDD-Storage/...'
|
|
```
|
|
|
|
Los archivos en `/DATA`/`/media/HDD-Storage` pertenecen al usuario
|
|
`devops` del host (via samba), pero los procesos dentro del
|
|
contenedor corren como `root` (UID 0) — git, desde la versión que
|
|
introdujo la protección CVE-2022-24765, rechaza operar sobre un repo
|
|
cuyo dueño no coincide con el UID del proceso, a menos que el path
|
|
esté explícitamente marcado como seguro.
|
|
|
|
**Fix:** `git config --global --add safe.directory '*'` dentro del
|
|
contenedor. El wildcard (en vez de listar cada repo) es intencional
|
|
acá: este contenedor ya tiene acceso root-equivalente al host vía el
|
|
volumen de `/var/run/docker.sock`, así que restringir `safe.directory`
|
|
por repo no suma aislamiento real.
|
|
|
|
**Por qué está en el comando de arranque y no como paso manual
|
|
aparte:** igual que los paquetes `apk`, esta configuración vive en el
|
|
filesystem efímero del contenedor (`/root/.gitconfig`, que no está
|
|
bajo el volumen de `/root/.claude`) — se perdería si el contenedor se
|
|
recrea. Ponerla en el `sh -c` de arranque la deja recreándose sola
|
|
cada vez, sin pasos manuales que recordar.
|
|
|
|
## Gotcha: la confianza del folder no persistía
|
|
|
|
Al abrir `claude` por primera vez en un directorio, pide confirmar un
|
|
diálogo de "¿confiás en este folder?" (más aún acá, porque este repo
|
|
trae un `.claude/settings.local.json` con permisos pre-aprobados). Esa
|
|
confirmación se guarda en `~/.claude.json` — un **archivo suelto en
|
|
`/root`**, no dentro de la carpeta `~/.claude/` que ya estaba montada
|
|
como volumen. Resultado: aunque el login (`~/.claude/.credentials.json`)
|
|
sí persistía, el diálogo de confianza volvía a aparecer cada vez que
|
|
se recreaba el contenedor — justo el tipo de "reconfigurar de nuevo"
|
|
que este setup existe para evitar.
|
|
|
|
**Cómo se detectó:** al confirmar el diálogo por primera vez, se
|
|
comparó `/root/.claude.json` dentro del contenedor contra sí mismo
|
|
antes y después — mismo tamaño y mismo `mtime`, es decir, la
|
|
confirmación en pantalla nunca se había registrado. La causa real:
|
|
la primera vez se movió el selector con las flechas pero nunca se
|
|
presionó Enter, así que en rigor el diálogo seguía sin confirmar; una
|
|
vez confirmado *de verdad* (`tmux send-keys` moviendo el cursor a
|
|
"Yes, I trust this folder" + Enter), el archivo saltó de 2.2 KB a
|
|
40 KB y apareció la clave `projects` con la ruta del repo adentro.
|
|
|
|
**Fix:** copiar ese `.claude.json` ya confirmado a
|
|
`/DATA/AppData/k3d-lab-v3/claude-code-home.json` en el host, y
|
|
montarlo **archivo a archivo** (no como carpeta) en
|
|
`-v .../claude-code-home.json:/root/.claude.json`. A diferencia de
|
|
los demás volúmenes de este setup, este necesitó sembrarse con
|
|
contenido real primero — un bind mount de archivo con un origen que
|
|
no existe todavía en el host termina creando un directorio vacío en
|
|
su lugar, no un archivo, y rompe el arranque del contenedor.
|
|
Verificado en vivo: tras recrear el contenedor con este mount, `claude`
|
|
arrancó directo en la sesión normal, sin volver a pedir el diálogo de
|
|
confianza.
|
|
|
|
Sobre paquetes: alpine (imagen `alpine` sin pin de versión, la misma
|
|
que uso este setup) sí trae `kubectl` en sus repos — no hizo falta
|
|
cambiar de imagen base ni instalarlo por binario aparte, contrario a
|
|
lo que se sospechaba inicialmente. `nodejs`/`npm` (v24/v11 al momento
|
|
de escribir esto) también están en los repos de alpine y cumplen de
|
|
sobra el mínimo que pide Claude Code.
|
|
|
|
**Nota sobre `--network host` + `docker.sock`:** el contenedor tiene,
|
|
en la práctica, control equivalente a root sobre el NAS (puede lanzar
|
|
cualquier otro contenedor con cualquier mount). Es una concesión
|
|
deliberada — es exactamente lo que hace falta para seguir operando
|
|
Docker/k3d desde dentro — no un descuido.
|
|
|
|
## Reconexión: un solo comando
|
|
|
|
```bash
|
|
/media/HDD-Storage/AppData/k3d-lab-v3/lab-cruzcloud/scripts/lab-session.sh
|
|
```
|
|
|
|
El script (`scripts/lab-session.sh`) es intencionalmente el mismo
|
|
comando sin importar el estado previo:
|
|
|
|
```bash
|
|
docker exec -it "$CONTAINER" tmux attach -t "$SESSION" || \
|
|
docker exec -it "$CONTAINER" tmux new -s "$SESSION"
|
|
```
|
|
|
|
Si la sesión `lab` existe, se adjunta a ella (mismo shell, mismo
|
|
`claude` corriendo, mismo directorio de trabajo). Si no existe —por
|
|
ejemplo justo después de que el contenedor se recreó desde cero— la
|
|
crea. No hay que recordar cuál de los dos casos aplica.
|
|
|
|
## Chequeo de salud automático (cada 30 min)
|
|
|
|
`scripts/tmux-host/healthcheck.sh` corre en background dentro del
|
|
propio contenedor (lanzado por `entrypoint.sh`, el `ENTRYPOINT` de la
|
|
imagen) y cada `HEALTHCHECK_INTERVAL_SEC` (1800s por defecto) hace dos
|
|
chequeos independientes, con una respuesta distinta según el tipo de
|
|
falla:
|
|
|
|
- **Conectividad de red real** (`curl` a `https://gitea.cruzcloud.net/`):
|
|
si falla, **solo se registra** en el log y se notifica por Telegram —
|
|
el contenedor nunca intenta "arreglar" la red del host. Un problema de
|
|
red puede ser de hardware/BIOS (deuda técnica ya conocida, ver
|
|
`incidente-medusa-crashloop-2026-08.md`), fuera del alcance real de un
|
|
contenedor.
|
|
- **Existencia de la sesión tmux `lab`** (`tmux has-session`): si no
|
|
existe, **sí se recrea automáticamente** (`tmux new-session -d -s lab
|
|
-c <WORKDIR>`, seguido de `tmux send-keys -t lab claude --remote-control
|
|
--continue Enter` — ver gotcha "sesión recreada pero sin Claude Code
|
|
adentro" más abajo, y los gotchas de Remote Control y `--continue`
|
|
agregados el 2026-09-20) — es solo recuperar el propio entorno de
|
|
trabajo del contenedor, no tocar infraestructura externa, así que es
|
|
seguro hacerlo sin intervención humana. Si `WORKDIR` todavía no está
|
|
montado (ver gotcha siguiente),
|
|
no se crea la sesión esa vuelta — se reintenta en el próximo ciclo en
|
|
vez de crearla mal posicionada.
|
|
|
|
Todo evento (chequeo ok, falla de red, sesión recreada) queda en
|
|
`/DATA/AppData/k3d-lab-v3/tmux-host/logs/health.log` — persistente en
|
|
`/DATA`, no solo en el stdout del contenedor (que se pierde con
|
|
`docker logs` al recrearlo). Las notificaciones de Telegram reusan el
|
|
mismo bot/`chat_id` ya configurado para `scripts/lab-agent/`
|
|
(`lab-agent.env`), en vez de duplicar credenciales entre los dos
|
|
agentes del lab.
|
|
|
|
Para probar el intervalo sin esperar 30 minutos, se puede levantar el
|
|
contenedor con `-e HEALTHCHECK_INTERVAL_SEC=60` (solo para pruebas en un
|
|
contenedor descartable — nunca cambiar esto en el `tmux-host` real sin
|
|
volver a 1800 después).
|
|
|
|
## Gotcha: sesión recreada, pero sin Claude Code adentro (reinicio completo del NAS, 2026-09-19)
|
|
|
|
Distinto al caveat de persistencia de sesión de más abajo (ese cubre "el
|
|
*proceso* muere pero el contenedor sigue vivo" — cerrar la sesión `lab` a
|
|
mano, o un crash del propio `claude`). Este es otro escenario: **el HOST
|
|
completo se apaga y vuelve a arrancar desde cero** (corte de luz, no un
|
|
`docker restart` ordenado) — reordena cuándo arrancan Docker, la red y los
|
|
montajes de `/DATA`/`/media/HDD-Storage` entre sí, algo que un
|
|
`docker restart` normal nunca ejercita porque el host ya estaba arriba.
|
|
|
|
Tras un corte de luz real, se encontró que el usuario se reconectó a la
|
|
sesión `lab` y encontró un shell vacío, no Claude Code corriendo. Dos
|
|
causas distintas, ambas corregidas:
|
|
|
|
1. **La sesión tmux SÍ se recreaba sola** (confirmado en `health.log`: el
|
|
contenedor arrancó y la sesión `lab` quedó recreada en cuestión de
|
|
segundos), pero `check_tmux_session()` nunca lanzó `claude` adentro —
|
|
solo garantizaba que existiera el *shell*, no que el proceso de Claude
|
|
Code estuviera corriendo. El diagrama de arquitectura de este mismo doc
|
|
siempre mostró `T --> CC["proceso claude"]` como si fuera automático,
|
|
pero el código nunca lo implementó — el hueco quedó invisible mientras
|
|
nadie pasó por un reinicio completo real del NAS (en un `docker
|
|
restart` normal, el `claude` que ya estaba corriendo dentro de la
|
|
sesión simplemente sigue vivo, porque ahí el proceso *no* muere).
|
|
**Fix:** `check_tmux_session()` ahora llama `launch_claude_in_session()`
|
|
(`tmux send-keys -t lab claude Enter`) justo después de crear la sesión
|
|
— solo cuando la crea de cero, nunca sobre una sesión que ya existe (no
|
|
se reinyecta `claude` sobre un pane con trabajo en curso).
|
|
2. **Riesgo de carrera no ejercitado todavía, pero real:** `WORKDIR`
|
|
(`/DATA/AppData/k3d-lab-v3/lab-cruzcloud`) llega vía el symlink absoluto
|
|
`/DATA/AppData -> /media/HDD-Storage/AppData` (ver el gotcha del symlink
|
|
más arriba). Si el storage externo (`/media/HDD-Storage`) tarda en
|
|
montarse en el host más de lo que tarda Docker en arrancar este
|
|
contenedor — plausible después de un corte de luz, no de un simple
|
|
restart —, `mkdir -p "$LOG_DIR"` fallaba en silencio (`set -u` sin
|
|
`-e`) y ese arranque quedaba *sin ningún rastro* en el log, y
|
|
`tmux new-session -c "$WORKDIR"` habría creado la sesión mal
|
|
posicionada (o fallado) contra un directorio que todavía no existe.
|
|
**Fix:** `wait_for_workdir()` reintenta con backoff (pasos de 5s, hasta
|
|
`STARTUP_MOUNT_WAIT_MAX_SEC=120` por defecto) a que `WORKDIR` exista
|
|
antes de tocar el log o crear la sesión; si sigue sin existir pasado
|
|
ese máximo, se registra el error explícitamente y se reintenta en el
|
|
siguiente ciclo (30 min) en vez de fallar una sola vez y quedarse así.
|
|
|
|
**Validado en vivo:** imagen reconstruida (`tmux-host:test-fix`) y probada
|
|
en un contenedor descartable aparte (mismos mounts de `/DATA` y
|
|
`/media/HDD-Storage`, sin tocar el `tmux-host` real) — `docker stop` +
|
|
`docker start` (arranque desde cero, no `restart`) recreó la sesión `lab`
|
|
con `claude` corriendo adentro sin intervención manual, en ~1 segundo.
|
|
|
|
## Gotcha: Remote Control nunca se activaba solo (2026-09-20)
|
|
|
|
Con la sesión `lab` recreándose sola y `claude` corriendo adentro (gotcha
|
|
anterior), quedaba un problema distinto y más sutil: la sesión servía
|
|
perfecto para trabajar vía `docker exec`/`lab-session.sh`, pero nunca
|
|
aparecía como controlable remotamente en claude.ai — como si Remote
|
|
Control jamás se hubiera activado.
|
|
|
|
**Causa raíz (confirmado en vivo 2026-09-20):** Remote Control es
|
|
opt-in por lanzamiento, vía el flag `claude --remote-control [nombre]`
|
|
de la CLI. Nada en `launch_claude_in_session()` lo pasaba nunca, así
|
|
que `remoteEnabled` en `~/.claude.json` quedaba en `false` para siempre
|
|
porque nadie lo pedía. No existe un paso separado de confirmación
|
|
"¿Activar Remote Control? y/n" más allá del diálogo normal de
|
|
confianza del folder (una hipótesis inicial de que sí existía tal paso,
|
|
y de que había una clave `remoteControlAtStartup` en `settings.json`,
|
|
resultó falsa al revisar directamente los archivos de configuración
|
|
reales).
|
|
|
|
**Cómo se detectó:** lanzar `claude --remote-control` a mano (en una
|
|
ventana de tmux en foreground, no vía un `!` de un solo uso que se
|
|
mata al terminar) sí registró la sesión y apareció en la lista de
|
|
sesiones activas de claude.ai como `zimaos-quizzical` — el hostname del
|
|
contenedor (`ZimaOS`) se usa como prefijo por defecto del nombre de
|
|
sesión remota.
|
|
|
|
**Fix:** cambiar la línea `tmux send-keys` en
|
|
`launch_claude_in_session()` para lanzar `claude --remote-control` en
|
|
vez de `claude` a secas.
|
|
|
|
**Validado en vivo:** tras reconstruir la imagen y swapear el
|
|
contenedor real (ver el gotcha del swap más abajo), `health.log`
|
|
registró `'claude --remote-control' lanzado...` al recrear la sesión, y
|
|
el pane de tmux mostró `/remote-control is active` con un link de
|
|
sesión válido — sin que nadie lo activara a mano.
|
|
|
|
## Gotcha: cada recreación de la sesión empezaba una conversación en blanco (2026-09-20)
|
|
|
|
Con Remote Control ya activándose solo, quedaba una fricción menor pero
|
|
molesta: cada vez que la sesión `lab` se recreaba (reinicio del NAS,
|
|
`docker rm` + recrear, o el propio healthcheck reaccionando a un
|
|
crash), `launch_claude_in_session()` arrancaba una conversación
|
|
completamente nueva. El contexto de trabajo en curso —no las
|
|
credenciales ni la confianza del folder, que sí persisten vía volumen—
|
|
se perdía cada vez, sin relación con los límites de cuota (ver la nota
|
|
al principio de este doc).
|
|
|
|
**Fix:** agregar `-c`/`--continue` (retoma la conversación más reciente
|
|
en el directorio de trabajo actual) al comando de lanzamiento:
|
|
`claude --remote-control --continue`. Si no hay ninguna conversación
|
|
previa en `WORKDIR`, el propio CLI arranca una nueva sin error — no
|
|
hace falta lógica condicional en el script.
|
|
|
|
**Validado en vivo — la prueba más fuerte posible:** el swap que
|
|
desplegó este mismo cambio (ver gotcha siguiente) mató la sesión de
|
|
Claude Code que lo estaba ejecutando. Del otro lado, la *misma
|
|
conversación* siguió sin cortarse, con el mismo ID de sesión, sin
|
|
ninguna intervención manual — evidencia directa de que `--continue`
|
|
efectivamente retoma la conversación y no solo que el contenedor volvió
|
|
a levantar.
|
|
|
|
## Gotcha: actualizar la imagen de `tmux-host` en caliente mata al propio proceso que hace el swap (2026-09-20)
|
|
|
|
Este contenedor tiene un problema estructural para actualizarse a sí
|
|
mismo: el proceso de Claude Code que ejecutaría `docker stop tmux-host
|
|
&& docker rm tmux-host && docker run ... tmux-host:nueva-imagen` corre
|
|
*dentro* del propio contenedor `tmux-host` — el primer `docker stop`
|
|
mata su propia shell a mitad de la secuencia, dejando el swap a medio
|
|
terminar (visto en vivo: un primer intento así falló en silencio).
|
|
|
|
**Fix:** correr la secuencia de swap desde un contenedor *sibling*
|
|
efímero, no desde dentro de `tmux-host`:
|
|
|
|
```bash
|
|
docker run --rm \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
-v /DATA/AppData/k3d-lab-v3/tmux-host:/tmux-host-cfg \
|
|
docker:cli sh /tmux-host-cfg/swap-retry.sh
|
|
```
|
|
|
|
Como este helper solo comparte el socket de Docker (no vive *dentro*
|
|
del contenedor `tmux-host`), detener y recrear `tmux-host` no lo afecta
|
|
a él ni al proceso que lo lanzó desde el host. El script
|
|
(`swap-retry.sh`, junto a sus logs, vive en
|
|
`/DATA/AppData/k3d-lab-v3/tmux-host/` — mismo path físico que
|
|
`/media/HDD-Storage/AppData/k3d-lab-v3/tmux-host/`, ver el gotcha del
|
|
symlink más arriba) deja un log fechado por corrida en `logs/` con el
|
|
`docker ps` y un `grep` sobre `healthcheck.sh` dentro de la imagen
|
|
nueva, para verificar sin tener que adivinar si el fix realmente quedó
|
|
adentro.
|
|
|
|
**Validado en vivo dos veces (2026-09-20):** el fix de Remote Control y
|
|
el de `--continue` se desplegaron cada uno con este mismo mecanismo,
|
|
ambos confirmados por `docker ps` (imagen e `CreatedAt` nuevos) y por
|
|
`health.log` mostrando la línea de lanzamiento esperada inmediatamente
|
|
después del swap.
|
|
|
|
## Qué sobrevive a qué
|
|
|
|
| Evento | ¿Sobrevive la sesión tmux (`lab`)? | ¿Sobrevive el login de Claude Code? | ¿Retoma la misma conversación? |
|
|
|---|---|---|---|
|
|
| Corte de SSH / cerrar la terminal / cambiar de dispositivo | Sí — es exactamente el caso que este setup resuelve | Sí | Sí — el proceso `claude` ni se entera |
|
|
| `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 |
|
|
|
|
## Validación en vivo
|
|
|
|
Confirmado en este mismo lab (2026-08-27): se dejó una variable de
|
|
entorno (`export MARCA_PERSISTENCIA=ok_<timestamp>`) corriendo dentro
|
|
de la sesión `lab` vía `tmux send-keys`, y una invocación
|
|
*completamente separada* de `docker exec` (equivalente a una nueva
|
|
conexión) leyó de vuelta el mismo valor — confirma que el estado vive
|
|
en el servidor tmux del contenedor, no en el cliente que se conecta.
|
|
`claude --version` respondió `2.1.250` dentro de esa misma sesión, y
|
|
un archivo de prueba escrito en `/root/.claude` dentro del contenedor
|
|
apareció de inmediato en
|
|
`/DATA/AppData/k3d-lab-v3/claude-code-home` en el host, confirmando
|
|
el bind mount de credenciales.
|
|
|
|
Después de agregar el mount de `/media/HDD-Storage` y el
|
|
`safe.directory`, se validó además: `md5sum` de `CLAUDE.md` idéntico
|
|
leído desde el host y desde dentro del contenedor (vía el path con
|
|
symlink `/DATA/AppData/...`), y `git log`/`git status` corriendo sin
|
|
error sobre `apps-registry` y `scripts` desde una shell dentro de
|
|
`tmux-host`.
|