diff --git a/MODAL.md b/MODAL.md new file mode 100644 index 0000000..7e8d561 --- /dev/null +++ b/MODAL.md @@ -0,0 +1,94 @@ +# Migrar las salas a Modal + +Estado: investigado, sin escribir codigo. Fecha: 30 de agosto de 2026. + +## Por que + +Multi no escala porque cada sala mantiene vivo un servidor de desarrollo +(un proceso de Node con el proyecto cargado en memoria) y **nadie lo apaga**. +Con 31 salas guardadas, las que despiertan se quedan corriendo aunque no haya +nadie mirando. + +El VPS actual (KVM 2: 2 vCPU, 8 GB) con los limites por defecto de 2 CPU y 2 GB +por sala da para **una sala a la vez**. Con 0.5 CPU y 768 MB daria cuatro. El +experimento del capitulo necesita hasta 20 salas simultaneas en la condicion +individual. + +## Como lo resuelven los demas + +- **Bolt**: no tiene el problema. WebContainers corre Node dentro del navegador + del usuario, asi que el preview no cuesta infraestructura. No aplica a Multi: + el preview es compartido entre varias personas. +- **Replit**: duerme los contenedores a los 5 minutos de inactividad, con + arranque en frio de 10 a 30 segundos al volver. +- **Lovable**: usa Modal Sandboxes. Un sandbox por sesion de generacion. En un + fin de semana corrieron mas de un millon de sandboxes, con 20 mil concurrentes + en el pico. Al migrar a Modal pasaron de 15 mil lineas de orquestacion a 700. + +## Lo que Modal resuelve + +Verificado en su documentacion, y **todo existe en el SDK de JavaScript**: + +- `sandboxes.create(app, image, opts)` con `timeoutMs`, volumenes, secretos +- `sb.exec([...])` para comandos, con stdout en streaming +- `sb.filesystem`: `readText`, `writeText`, `listFiles`, `stat`, + `makeDirectory`, `remove`, `copyFromLocal`, `copyToLocal` +- `encryptedPorts` + `sb.tunnels()` para exponer el dev server por HTTPS +- `sb.createConnectToken()` para HTTP/WebSocket autenticado hacia el sandbox +- **`idleTimeout`**: el sandbox muere solo tras inactividad, y una conexion TCP + abierta en un tunel cuenta como actividad. Mientras alguien mire el preview, + la sala vive; cuando todos se van, se apaga sola. +- Named Sandboxes: un nombre unico por app, solo uno corriendo a la vez. Es lo + mismo que hoy hace el mutex de arranques con `multi-room-`. +- `sandboxes.fromId()` / `fromName()`: la sala sobrevive a un reinicio del server +- Aislamiento con gVisor, mas fuerte que Docker normal + +Precio: por segundo de uso, ~0.0000394 USD por nucleo-segundo. **Un sandbox +apagado no cuesta**, que es lo contrario de hoy, donde 31 salas dormidas cuestan +lo mismo que 31 activas. + +Solo Python: definir Modal Functions. No hace falta para esto. + +## Que cambia en el motor + +`Runner` (`engine/runner.ts`) ya es la abstraccion correcta: una interfaz con dos +implementaciones (`containerRunner` y `localRunner`) y quien la usa no sabe cual +le toco. **Agregar `modalRunner(roomId)` es un tercer brazo con la misma forma.** + +Lo que no tiene abstraccion todavia y hay que resolver: + +1. **Archivos.** `agent/tools/fs.ts`, `engine/file-mutation.ts` y `engine/git.ts` + leen y escriben disco local directo. Con Modal el proyecto vive alla. +2. **El CAS.** El `read-compare-write` de `writeIfUnchanged` es atomico porque + pasa en el mismo proceso. Con `sb.filesystem` son llamadas de red: el mutex + sigue sirviendo (vive en nuestro server), pero cada operacion cuesta un viaje. +3. **Git.** Hoy corre en el disco del host. Con Modal, o corre dentro del sandbox + por `exec`, o se mantiene una copia local sincronizada. Hay que elegir. +4. **El preview.** `engine/preview.ts` y `engine/proxy.ts` asumen + `localhost:PUERTO`. Con Modal es una URL de tunel remota. + +## Plan + +**Paso 1 — prueba de concepto, sin tocar el motor.** Un script suelto que: +- cree un sandbox y cronometre cuanto tarda +- corra `npm create vite` adentro y levante el dev server +- abra el tunel y verifique que carga en el navegador +- **mida la latencia** de `readText`, `writeText` y `exec` + +Esos numeros deciden si el CAS es viable: si cada lectura son 200 ms, un turno +que lee diez archivos se vuelve lento. + +Requiere: cuenta en modal.com y token (`modal token new`). + +**Paso 2 — solo si los numeros salen bien.** Rama nueva, `modalRunner` como +tercer brazo, y despues los archivos y el preview. + +## Lo que NO hay que hacer + +**No migrar antes del experimento.** Esto toca las tres partes mas delicadas del +motor (archivos, ejecucion y preview) y el experimento tiene fecha y grupos +prestados. Si falla ese dia, se pierde el experimento. + +Camino seguro para llegar a las pruebas: dormir salas + limites de 0.5 CPU y +768 MB + KVM 4 el mes del experimento + dos sesiones de 10 personas en vez de +una de 20. diff --git a/docs-preview-zombi.md b/docs-preview-zombi.md new file mode 100644 index 0000000..f8f2712 --- /dev/null +++ b/docs-preview-zombi.md @@ -0,0 +1,100 @@ +# El preview que no arranca por un dev server zombi + +Ocurrio UNA vez, el 1 de septiembre de 2026, en la sala `pixel-jam-25`. +**No es reproducible todavia.** Este documento existe para no investigar desde +cero si vuelve. + +## Sintoma + +La sala se queda en el spinner del preview para siempre. En los logs: + +``` +[sala X] falló el preview: Error: timeout esperando el dev server en :32816 +``` + +y en el reintento: + +``` +[preview X] error when starting dev server: +[preview X] proceso terminó (code 1) +[sala X] falló el preview: Error: dev server terminó antes de responder (code 1) +``` + +## Que se encontro + +Dentro del contenedor habia un Vite VIVO desde el primer intento, ocupando el +puerto interno: + +``` +root 180 node /work/node_modules/.bin/vite # arrancado 8 minutos antes +``` + +Ese proceso se habia atado solo a `localhost` (su log decia +`Network: use --host to expose`), asi que Docker publicaba el puerto pero no +habia nada alcanzable desde el host. Multi espero los 5 minutos del timeout, +se rindio, y **no mato el proceso**. + +## Por que se hizo visible ahi y no antes + +El proyecto tenia `strictPort: true` en su `vite.config.ts`. Sin esa opcion, +Vite SALTA al siguiente puerto libre cuando encuentra uno ocupado, asi que el +zombi pasa desapercibido: el preview arranca igual, solo que en otro puerto. +El comentario de `preview.ts:150` ya describia ese caso ("5 procesos peleando y +un preview que no reflejaba los cambios"). + +Con `strictPort`, Vite no salta: muere con codigo 1. El zombi dejo de ser +invisible. + +## La secuencia completa + +1. Aparece el `package.json` → el motor arranca el preview de inmediato +2. En ese momento el `vite.config.ts` **todavia no existe**: ese Vite sale sin + `--host` y no es alcanzable desde fuera del contenedor +3. Multi espera 5 minutos, falla, y deja el proceso vivo +4. El agente termina de escribir el `vite.config.ts`, ahora con `strictPort` +5. Todo reintento posterior choca con el zombi y muere con codigo 1 + +## Lo que NO es + +- **No es "proyecto pesado".** Un YouTube con React arranco sin problema. +- **No es el motor pasando mal los flags.** En una sala sana el proceso corre + como `vite --host --port 5173`, pero esos flags **los escribe el agente** en + su script de `package.json`, no el motor. +- **No es Caddy ni el proxy.** El 503 venia de que Multi no tenia dev server al + cual reenviar. + +## Como se arreglo a mano + +```bash +docker restart multi-room- # mata el zombi y libera el puerto +systemctl restart multi # OJO: hace falta, ver abajo +``` + +El `docker restart` **cambia el puerto publicado** (Multi crea los contenedores +con `-p 0:5173`, o sea "Docker elige"). Multi se queda con el viejo en memoria y +la sala sigue rota hasta reiniciar el servicio. Existe `readPublishedPort`, asi +que releerlo al reconectar seria el arreglo, si esto llega a repetirse. + +## Por que no se arreglo + +`killDevServersIn` YA existe y YA corre antes de cada arranque +(`preview.ts:150`). Si funcionara, el zombi habria muerto en el reintento de las +03:41 y no murio. **Puede que el bug este dentro de esa funcion**, no en donde +se llama, y en ese caso agregar otra llamada no arregla nada. + +Con una sola ocurrencia y sin reproduccion, cualquier cambio es una apuesta +contra un camino que funciona el resto de las veces. + +## Si vuelve a pasar + +Antes de tocar nada, recoger esto: + +```bash +docker exec multi-room- sh -c "ps aux | grep '[n]ode.*vite'" +docker port multi-room- +docker exec multi-room- sh -c "curl -sI http://localhost:5173 | head -2" +journalctl -u multi --no-pager -n 60 | grep -i "" +docker exec multi-room- sh -c "cat /work/package.json; cat /work/vite.config.*" +``` + +Con dos ocurrencias y sus datos ya se puede ver que tienen en comun. diff --git a/server/src/agent/loop.ts b/server/src/agent/loop.ts index fd7740e..37235a5 100644 --- a/server/src/agent/loop.ts +++ b/server/src/agent/loop.ts @@ -22,9 +22,14 @@ No trabajas en privado. Hay humanos mirando la pantalla mientras escribes, y pue haber otros agentes trabajando al mismo tiempo en el mismo proyecto. Todo lo que tocas aparece al instante en el preview que todos ven. -Dos consecuencias prácticas: -- Quien te habla puede no ser programador. Responde en el idioma en que te escriben - y en términos de lo que se ve, no de nombres de archivo. +Tres consecuencias prácticas: +- SIEMPRE respondes en el idioma del último mensaje que te escribieron. Si te + escriben en inglés, respondes en inglés; si te escriben en español, en español. + Da igual que estas instrucciones estén en español: son para ti, no para la sala. + Por qué: una sala se comparte por enlace y entra quien sea. Contestarle en otro + idioma a quien acaba de llegar es la forma más rápida de que se vaya. +- Quien te habla puede no ser programador: responde en términos de lo que se ve, no + de nombres de archivo. - Si un archivo cambió desde que lo leíste, la escritura falla y te lo dicen. Es otro agente trabajando, no un error tuyo: lee el archivo otra vez y reaplica tu cambio sobre lo que ahora hay.