La Parte 1 construyó una caja siempre encendida para una sesión de remote-control de Claude Code y terminó con una alarma que convierte seis días de silencio en quince minutos. Esa alarma atrapa una caja muerta. Esta es la secuela, y la caja de aquí nunca estuvo muerta. Estaba active, su token estaba fresco, y en cada reinicio imprimía:
Take this session with you and pick up right where you left off on any device.
Open the Code tab in the Claude mobile app, or visit claude.ai/code in a browser.
y la sesión no estaba en claude.ai/code. Nunca se había registrado. El banner de arriba se imprime al arrancar, antes de que haya pasado lo que anuncia, así que le mintió al monitoreo, me mintió a mí cuando lo "arreglé", y casi se mintió hasta meterse en este blog post como el arreglo. Este post es sobre la diferencia entre un proceso que dice que está listo y una sesión que de verdad lo está.
TL;DR
| Lo que creía | Lo que era cierto | |
|---|---|---|
| Causa raíz | el registro se puso viejo a lo largo de ~6 días de uptime | nunca se registró, el CLI se colgó en un prompt interactivo sin TTY |
| El "arreglo" | reinicia el servicio; el banner regresa; el heartbeat en verde | el reinicio imprimió el banner otra vez y aun así no se registró |
| La señal de salud que construí | grep del journal por claude.ai/code
|
ese string es el banner de pre-registro, verde para un proceso colgado |
| La señal real | la URL registrada (claude.ai/code/session_…), que solo se imprime después del registro |
Placeholders, como en la parte 1: myapp, appuser, acme/myapp.
El síntoma
La sesión simplemente no estaba en la lista de claude.ai/code, mientras que todo en la caja decía que debería estar:
$ systemctl is-active claude-remote-control
active
$ stat -c %y ~/.claude/.credentials.json
2026-07-28 02:54:11 # token refrescado hace horas, la auth está bien
$ aws cloudwatch describe-alarms --alarm-names myapp-agent-box-down \
--query 'MetricAlarms[].StateValue' --output text
OK
Así que hice lo obvio: reinicié el servicio. Imprimió el banner, el proceso subió, el heartbeat se puso verde, y la sesión seguía sin estar ahí. Ese es el momento en el que vale la pena detenerse. Cada señal que tenía decía sano, y el producto real (una sesión que puedo abrir desde un navegador) no existía.
Un reinicio que produce un banner no es un reinicio que produce una
sesión.
Conseguir una señal que no puede mentir
systemctl is-active reporta que el proceso existe. El banner reporta que el proceso arrancó. Ninguno reporta que la caja se registró con el punto de encuentro hospedado, lo único que mete la sesión en la lista. Para ver eso, pídele al CLI que escriba su propio log de debug y lee lo que de verdad pasa:
claude remote-control --name myapp-cloud --continue --debug-file /tmp/rc.log --verbose
Dos fallas cayeron de inmediato, y ninguna era "vieja":
Take this session with you and pick up right where you left off on any device.
Open the Code tab in the Claude mobile app, or visit claude.ai/code in a browser.
The session keeps running on this machine. ... Press Ctrl+C to stop.
Enable Remote Control? (y/n)
El proceso imprime el banner y luego se bloquea en un prompt interactivo.
Bajo systemd no hay TTY que lo conteste, así que espera ahí para siempre: active, banner emitido, registro ni siquiera intentado. Los CLI recientes agregaron esta confirmación; la caja de la parte 1 es anterior a ella.
Y cuando le di una y, la segunda falla:
Error: No recent session found in this directory or its worktrees.
Run `claude remote-control` to start a new one.
--continue, la bandera de la parte 1 que mantiene el marcador estable reanudando la misma sesión, da error duro cuando no hay sesión que reanudar. En una caja cuyo historial de sesión local se había borrado en una reconstrucción, --continue no era una optimización; era un arranque que solo podía fallar.
El arreglo real: contesta el prompt, y no dependas de que una sesión exista
Las dos fallas viven en una línea, así que el arreglo es una línea, un wrapper en ExecStart:
ExecStart=/bin/bash -c 'cd /home/appuser/myapp; \
echo y | /home/appuser/.local/bin/claude remote-control --name myapp-cloud --continue || \
echo y | /home/appuser/.local/bin/claude remote-control --name myapp-cloud'
Tres cosas se ganan su lugar:
-
echo ycontesta el prompt. Importa que el proceso siga corriendo después de que su stdin se cierra (verificado, sí lo hace), así que unechode una sola vez basta; sinyesinundando el pipe, sinsleep infinitymanteniéndolo abierto. -
--continue || <nuevo>trata de reanudar (marcador estable) y cae de vuelta a una sesión nueva cuando no hay nada que reanudar. Comoecho ycierra limpio, un--continuefallido de verdad regresa y corre el fallback; unsleep infinityaquí colgaría el pipeline en el camino de falla y reintroduciría el atoro silencioso exacto. -
Sin
$@ni$VARenExecStart. systemd expande${}y$VARpelón él mismo, y mutila$@; la jugada segura es repetir la ruta en lugar de factorizarla en una variable.
Ahora el log de debug muestra lo que el banner solo fingía:
[bridge:init] Resuming session session_01Pp8… on environment env_01Vp…
[bridge:init] Registered, server environmentId=env_01Vp…
[bridge:title] server title: myapp-cloud
Registered. Ese es el recibo. El banner era el menú.
Defensa en profundidad, todavía vale la pena conservarla
El wrapper arregla el outage. Dos guardas del primer borrador de este trabajo se quedan, porque endurecen la caja contra otras maneras en que la sesión puede caducar:
Reciclado diario. RuntimeMaxSec=86400 rebota la unidad cada 24h; Restart=always la trae de vuelta y --continue reanuda la misma sesión, así que el marcador aguanta. Si un registro alguna vez se pone viejo en un uptime largo, esto le pone tope al radio de impacto en un día en lugar de confiar en
que viva para siempre. TimeoutStopSec=20 mantiene cada reciclado rápido, ya que el CLI ignora SIGTERM por ~90s.
Escritura de vuelta de credenciales. La parte 1 señaló que el CLI rota su refresh token y la captura del secreto se pudre. Un helper chico empuja el token vivo de vuelta a Secrets Manager en cada corrida del watchdog, protegiendo con fuerza contra escribir un archivo local malformado encima de un secreto bueno, para que una instancia reemplazada ya no arranque sin autenticar.
El health check al que engañó el mismo banner
Aquí está la parte que arde. La primera versión del chequeo de readiness, la que se suponía atrapaba justo este estado de "arriba pero sin registrar", hacía esto:
# MAL: hace match al banner de pre-registro, así que está verde para un proceso colgado
journalctl -u claude-remote-control --since "${AGE}s ago" | grep -q "claude.ai/code"
claude.ai/code está en el banner que se imprime antes del prompt en el que el proceso estaba colgado. El chequeo que construí para detectar la falla quedaba satisfecho por la falla. Es el bug original vestido con el disfraz de su propio arreglo.
La sesión registrada imprime una línea distinta, una URL con un id de sesión o de environment:
Continue coding in the Claude mobile app or https://claude.ai/code/session_01Pp8…
El discriminador entre "anunciada" y "registrada" es un solo carácter: lo que sigue a code. El banner dice code in a browser; la cosa real dice code/session_… o code?environment=…. Así que haz match a eso:
# solo una sesión REGISTRADA imprime una URL con '/' o '?' después de 'code'
journalctl -u claude-remote-control --since "${AGE}s ago" | grep -qE 'claude\.ai/code[/?]'
Con el chequeo corregido el heartbeat significa lo que dice: active, lo bastante joven para haberse registrado, y una URL de registro en su propio log.
Cualquier otra cosa degrada el latido a DEGRADED y reinicia, y DEGRADED no hace match al filtro OK de la parte 1, así que un estado que no se va a auto-sanar de todos modos le suena a la alarma existente.
Callejones sin salida (los dos el mismo error)
- Checar por una conexión de red abierta. La sonda de readiness intuitiva: seguramente una sesión registrada sostiene un socket al punto de encuentro. Medido en la caja sana: cero sockets establecidos. Una sesión registrada inactiva no mantiene ninguno. El chequeo reiniciaría una caja funcionando en cada tick.
- Grep del banner de arranque. Cubierto arriba. Verde antes de que el trabajo esté hecho.
Los dos fallan de la misma manera: afirman sobre algo que es cierto antes de
que pase la cosa real. Un socket que quizá todavía no esté ahí; un banner que
está ahí demasiado pronto. El arreglo en los dos casos es afirmar sobre el
artefacto que existe solo después del éxito: la línea Registered, la URL
con un id.
Lecciones
- Un banner es un menú, no un recibo. Cualquier mensaje de "ya puedes hacer X" impreso al arrancar es una intención, no una confirmación. Los health checks tienen que indexarse en el artefacto que existe solo una vez que X de verdad pasó.
- Reiniciar hasta que reaparezca el mensaje feliz no es verificación. Vi el banner, vi el heartbeat ponerse verde, y seguí adelante, dos veces. El mensaje era el mismo las dos veces porque nunca dependió del éxito.
-
Los prompts interactivos son minas en cajas sin atender. Una sola
compuerta
(y/n)nueva, incontestable sin un TTY, convierte en silencio un servicio que funciona en uno colgado. Dale la respuesta explícitamente; no asumas que un servicio hereda la de tu terminal. -
Las banderas de conveniencia tienen modos de falla.
--continuemantiene un marcador estable y falla duro cuando no hay nada que continuar. En una caja que se puede reconstruir desde nada, "reanudar" necesita un "o arranca de nuevo" al lado. - Cuando no hay API de estado, afirma sobre el log de éxito, el específico. No el banner optimista. La línea que solo el camino exitoso puede producir.
Top comments (0)