Blog
El servidor como custodio del historial: diseño de recuperación de conversaciones para agentes de IA en móvil
Por qué Tactic Remote 1.8.2 trasladó el historial de chat del teléfono al servidor, y cómo esa decisión eliminó de raíz toda una categoría de problemas de orden incorrecto e historial perdido.
Sigue las novedades de producto e ingeniería de esta categoría.
Ver categoríaEste artículo trata sobre una decisión de arquitectura: por qué Tactic Remote 1.8.2 trasladó el historial de conversación del dispositivo móvil al servidor, y cómo esa decisión hizo desaparecer desde la raíz una serie de problemas que llevábamos parcheando repetidamente.
Un enfoque condenado al fracaso
Antes de la versión 1.8.2, el historial de chat lo ensamblaba el móvil en tiempo real. El servidor enviaba los eventos de streaming del agente al teléfono, y el teléfono los iba convirtiendo en burbujas de mensaje para formar la conversación que ves en pantalla.
En el escritorio, este enfoque funcionaba razonablemente bien porque la conexión es estable. Pero en móvil tiene un defecto de fondo: el historial solo existe dentro de la conexión y la sesión actuales. Y así surgían los problemas uno tras otro.
- Al reconectar, la nueva conexión no tenía acceso a los eventos anteriores: el historial aparecía en blanco o incompleto.
- Al cerrar y reabrir la aplicación, la conversación almacenada en memoria desaparecía y había que empezar de cero.
- Cuando varios dispositivos se conectaban a la misma sesión, cada uno solo veía los eventos ocurridos desde su propia conexión, como si fueran conversaciones paralelas.
Había además un problema más sutil relacionado con el orden. Al enviar un mensaje, el móvil mostraba la burbuja del usuario de forma optimista, mientras que la primera respuesta del agente no llegaba hasta que su proceso arrancaba. Cualquier pequeña variación en el orden de llegada —por ejemplo, mensajes en cola que llegaban tarde tras una reconexión— podía hacer que la burbuja del usuario apareciera por debajo de la respuesta. Corregimos ese problema de «la burbuja del usuario cae al fondo» varias veces, pero siempre era un parche superficial. La causa raíz era estructural: en todo el sistema, nadie era responsable del orden verdadero de la conversación.
La filosofía de tmux
El modo terminal de Tactic Remote siempre ha sido estable gracias a tmux. El modelo de tmux es simple y poderoso: el búfer del terminal es la única fuente de verdad. Al conectarte (attach), esa fuente simplemente te reproduce lo que ha ocurrido. Desconexión, cambio de dispositivo, reinicio: todo consiste en volver a unirse al mismo búfer, y el historial sigue ahí.
Eso es exactamente lo que le faltaba al modo chat. Por eso el diseño de la versión 1.8.2 traslada esa misma filosofía:
El servidor mantiene un registro canónico de la conversación, y cuando el cliente se conecta, ese registro se restaura completamente.
Una vez que el servidor es la autoridad en cuanto al orden, los problemas anteriores dejan de ser bugs que hay que corregir uno a uno: estructuralmente ya no pueden producirse.
Cómo funciona en la práctica
En el servidor: un búfer circular por sesión
El servidor mantiene un registro de conversación para cada sesión de chat. Cada evento difundido se añade en orden a un búfer circular con dos límites: uno en número de eventos y otro en bytes totales; cuando se supera alguno, se recortan los eventos más antiguos. Sin embargo, el evento de inicialización que marca el comienzo de la sesión —el ancla de la recuperación— nunca se descarta. Cuando se produce un recorte, el búfer queda marcado como «truncado», de modo que al recuperar la sesión el cliente pueda mostrar honestamente «los mensajes anteriores no están disponibles», en lugar de simular que el historial está completo.
Un paso clave: los mensajes enviados por el propio usuario ahora también generan un evento con número de secuencia en el servidor y quedan escritos en ese registro. Antes, los mensajes de usuario solo existían en la representación optimista del móvil y el servidor no tenía constancia de ellos; los mensajes de dirección insertados a mitad de una sesión (steer) ni siquiera llegaban a registrarse. Ahora que el servidor es la autoridad del orden, cada turno del usuario queda ubicado en su posición real. El problema de «la burbuja del usuario cae al fondo» desaparece de forma estructural.
Recuperación: un sobre en lugar de reenviar los eventos originales
La primera propuesta era reenviar los eventos originales al reconectar. Se descartó rápidamente. Los clientes más antiguos ignorarían los eventos que no reconocieran, pero los que se reenviaban eran eventos que sí conocían, así que los tratarían como mensajes nuevos en tiempo real y los añadirían de nuevo: el historial se duplicaría.
La solución adoptada fue empaquetar el registro completo dentro de un nuevo tipo de evento: structured_replay, que incluye número de secuencia, indicador de si es el último lote, indicador de truncamiento, estado de ejecución actual y la lista de eventos. Los clientes antiguos no reconocen este evento y lo ignoran sin consecuencias; los clientes nuevos abren el «sobre», alimentan los eventos contenidos a través del mismo flujo de procesamiento habitual y reconstruyen la conversación completa.
Dado que el límite de recepción por fotograma de WebSocket en iOS es de aproximadamente 1 MiB, la recuperación se envía en fragmentos de 256 KB, numerados para mantener el orden. Las aprobaciones de herramientas ya procesadas se restauran marcadas como «aprobado / rechazado», para que una tarjeta de aprobación histórica no se renderice erróneamente como si estuviera pendiente de acción.
En el cliente: reconstrucción al conectar, con efectos secundarios bloqueados
Cuando el cliente recibe el primer lote de recuperación —aquel con número de secuencia 0—, borra la conversación local y alimenta los eventos de la lista a través de la misma función de procesamiento para reconstruirla. Es decir, la recuperación sigue exactamente el mismo camino de renderizado que los eventos en tiempo real: no hay que escribir lógica aparte.
Hay un detalle que no se puede obviar: durante la recuperación, hay que bloquear los efectos secundarios que solo deben dispararse en contexto de tiempo real —el envío automático de la cola, el perro guardián de tiempo de espera, el cambio de estado de ejecución, la activación automática de las aprobaciones—. Un indicador los suprime todos hasta que llega el último lote, momento en que se adopta el estado de ejecución canónico incluido en el sobre.
En cuanto a la burbuja optimista del usuario al enviar, decidimos conservarla. Sin ella habría un instante de pantalla en blanco mientras el proceso del agente arranca, lo que no es una buena experiencia. Lo que hacemos es convertirla en un marcador «pendiente de confirmación»: en cuanto llega el acuse de recibo del servidor, ese marcador se elimina y el mensaje se vuelve a insertar en su posición real según el registro canónico. El desorden deja de poder persistir de forma estructural.
Compatibilidad: por qué «ignorar eventos desconocidos» es una decisión de diseño
Que el esquema de recuperación pudiera desplegarse de forma segura se debe en gran medida a una convención establecida desde el principio: el cliente ignora sin más cualquier evento que no reconozca. Esto permite al servidor introducir nuevos tipos de eventos como structured_replay sin perturbar en absoluto a los clientes que aún no han actualizado. Esos clientes no ven la funcionalidad de recuperación, pero tampoco se rompen por ello.
En un sistema donde la app de iOS, los servidores de Mac, Linux y Windows, y un CLI de npm se publican de forma independiente y conviven con versiones muy dispares, esta disciplina de protocolo —solo añadir, nunca modificar; ignorar lo desconocido— es lo que permite que el servidor se adelante y los clientes se vayan poniendo al día.
Límites y compromisos honestos
Este diseño tiene sus límites, y preferimos explicarlos con claridad.
- El búfer del servidor tiene un tope. En sesiones muy largas, el fragmento más antiguo del historial puede recortarse; al recuperar, se muestra explícitamente «los mensajes anteriores no están disponibles», sin fingir que el historial está completo.
- Las sesiones recuperadas mediante
claude --resumeempiezan de cero. Si una sesión lleva un tiempo sin suscriptores, el servidor la recicla; cuando se recupera conclaude --resume, el búfer arranca vacío y el historial anterior ya no se reproduce. Es un compromiso deliberado: limitar el historial retenido a cambio de un consumo de memoria controlado. - La persistencia local en el cliente es una mejora futura. Por ahora, el registro canónico solo existe mientras el servidor está en línea. Guardar el historial también en el dispositivo es una mejora planificada para versiones posteriores, no algo incluido en esta versión.
Preferimos documentar estos límites con antelación antes de que los descubras un día al ver que falta un tramo de historial y tengas que adivinar el motivo.
¿Quieres experimentar el efecto «conectar y recuperar»? La app de iOS está disponible gratuitamente en el App Store, y tanto la app complementaria como el CLI son también gratuitos. Si te interesa el otro gran desafío de esta versión —esa congelación de la interfaz que no estaba en el código de negocio—, puedes leer «Cazando un bloqueo de interfaz».
Prueba Tactic Remote
Controla tus agentes de programación desde el móvil
Conéctate a Claude Code, Codex y otros agentes en tu Mac, Windows o Linux. Consulta el progreso y envía la siguiente instrucción desde el iPhone o iPad.