From 114e7b4a20b7c692f271cebb365a45029b84dfac Mon Sep 17 00:00:00 2001 From: ErickHub192 Date: Tue, 1 Sep 2026 12:12:57 -0600 Subject: [PATCH 1/2] Dos notas: escalar a Modal y el preview zombi MODAL.md deja escrito por que Multi no escala hoy (nadie apaga los dev servers de las salas), como lo resuelven Bolt, Replit y Lovable, y que se rompe del motor si se migra. Verificado en la documentacion de cada uno. docs-preview-zombi.md documenta un fallo que ocurrio UNA vez y no es reproducible: un dev server que sobrevive al timeout y deja el puerto ocupado. No se arreglo a proposito, y el archivo dice por que: la funcion que deberia limpiarlo ya existe y ya corre, asi que el bug puede estar adentro de ella. Con una sola ocurrencia, tocar el camino que funciona el resto de las veces es una apuesta. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BBNGubbzvoNEBs8268pzzo --- MODAL.md | 94 +++++++++++++++++++++++++++++++++++++++ docs-preview-zombi.md | 100 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 194 insertions(+) create mode 100644 MODAL.md create mode 100644 docs-preview-zombi.md 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. From 985e43eb0d67c7cc0eb92eb9a1229851a45e1a8a Mon Sep 17 00:00:00 2001 From: ErickHub192 Date: Tue, 1 Sep 2026 12:29:18 -0600 Subject: [PATCH 2/2] El agente contesta en el idioma en que le escriben MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit La regla ya existia, pero colgaba de otra ("quien te habla puede no ser programador") y se perdia. Probado en una sala vacia: mensaje en ingles, respuesta entera en español. Todo el prompt esta escrito en español, asi que el sesgo del modelo pesa mas que una linea de paso. Ahora es regla propia y dice explicitamente que el idioma de estas instrucciones no es el de la sala. Importa para una sala que se comparte por enlace: contestarle en otro idioma a quien acaba de llegar es la forma mas rapida de que se vaya. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01BBNGubbzvoNEBs8268pzzo --- server/src/agent/loop.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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.