Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions MODAL.md
Original file line number Diff line number Diff line change
@@ -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-<id>`.
- `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.
100 changes: 100 additions & 0 deletions docs-preview-zombi.md
Original file line number Diff line number Diff line change
@@ -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-<sala> # 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-<sala> sh -c "ps aux | grep '[n]ode.*vite'"
docker port multi-room-<sala>
docker exec multi-room-<sala> sh -c "curl -sI http://localhost:5173 | head -2"
journalctl -u multi --no-pager -n 60 | grep -i "<sala>"
docker exec multi-room-<sala> sh -c "cat /work/package.json; cat /work/vite.config.*"
```

Con dos ocurrencias y sus datos ya se puede ver que tienen en comun.
11 changes: 8 additions & 3 deletions server/src/agent/loop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading