=== Transcripción del agente (turno a turno) ===
args de write_file con el cuerpo omitido (N1); outputs recortados (N2).

[turno 1] bash  {"command": "ls -la /workspace && cat /workspace/README* 2>/dev/null"}
    exit=0
    --- stdout ---
    total 80
    drwxr-xr-x 4 root root  4096 Jul 13 12:26 .
    drwxr-xr-x 1 root root  4096 Jul 13 12:12 ..
    -rw-r--r-- 1 root root  4199 Jul 13 12:25 DECISIONS.md
    -rw-r--r-- 1 root root  2399 Jul 13 12:12 README.md
    -rw-r--r-- 1 root root  2943 Jul 13 12:12 api.md
    -rw-r--r-- 1 root root  1831 Jul 13 12:20 api_v2.md
    -rw-r--r-- 1 root root 19523 Jul 13 12:22 app.py
    drwxr-xr-x 2 root root  4096 Jul 13 12:23 backups
    drwxr-xr-x 2 root root  4096 Jul 13 12:25 data
    -rwxr-xr-x 1 root root   124 Jul 13 12:14 run.sh
    -rw-r--r-- 1 root root  8885 Jul 13 12:18 test_api.py
    -rw-r--r-- 1 root root  5827 Jul 13 12:22 test_api_v2.py
    # Sistema de reservas para "Marea"
    
    Somos un restaurante con **14 mesas** y dos turnos (**comida** y **cena**).
    Hasta hoy las reservas se gestionan por teléfono y libreta, y ya no damos abasto:
    dobles reservas apuntadas a boli, gente que llama dos veces, huecos que se pierden.
    
    Necesitamos un sistema de reservas interno para el equipo de sala. Diséñalo e
    implemént
    …(N2: 522 chars omitidos en medio)…
    ueda apuntado en lista de
      espera por orden de llegada. Si alguien cancela, el primero de la lista entra
      automáticamente.
    - **Modificar una reserva** (personas, fecha o turno). Si el cambio no cabe, la
      reserva original NO se pierde: se queda como estaba y se avisa.
    - **Cancelar una reserva** (libera la mesa y promociona la lista de espera).
    - **Consultar disponibilidad** de una fecha y turno.
    - **Saber si un teléfono ya tiene reserva** (nos llaman y no se acuerdan).
    
    El mismo cliente despistado envía la reserva dos veces seguidas, o dos personas de la
    misma familia reservan a la vez para lo mismo: **el mismo teléfono no puede tener dos
    reservas activas para la misma fecha y turno**.
    
    ## Contrato técnico (NO negociable)
    
    La interfaz HTTP exacta (endpoints, códigos de estado, forma del JSON, idempotencia y
    límite de peticiones) está especificada en **`api.md`**. Respétala al pie de la letra:
    nuestro panel de sala se integra contra ella. Todo lo interno es cosa tuya.
    
    ## Cómo la arrancamos
    
    Deja un script ejecutable **`run.sh`** en la raíz que levante tu app escuchando en
    `0.0.0.0:8080` en primer plano:
    
    ```
    sh run.sh    # la app responde en http://localhost:8080
    ```
    
    Durante el desarrollo tienes internet para instalar dependencias. En nuestro entorno
    de despliegue la app corre **sin salida a internet**: no dependas de servicios externos.
    
    ## Nota
    
    El restaurante va bien y el dueño tiene planes. El sistema tendrá que evolucionar;
    no sabemos aún hacia dónde.
    
    --- stderr ---

[turno 1] bash  {"command": "find /workspace -maxdepth 3 -type f | head -50"}
    exit=0
    --- stdout ---
    /workspace/README.md
    /workspace/app.py
    /workspace/api.md
    /workspace/backups/reservas.db.bak.1783945283
    /workspace/test_api_v2.py
    /workspace/data/reservas.db-wal
    /workspace/data/reservas.db-shm
    /workspace/data/reservas.db
    /workspace/run.sh
    /workspace/api_v2.md
    /workspace/DECISIONS.md
    /workspace/test_api.py
    
    --- stderr ---

[turno 2] read_file  {"path": "app.py"}
    #!/usr/bin/env python3
    """Sistema de reservas "Marea" — v1.
    
    Stack: Python 3 (stdlib) + SQLite. Sin dependencias externas.
    Contrato HTTP: ver api.md (congelado).
    """
    
    import json
    import os
    import re
    import sqlite3
    import threading
    import time
    import uuid
    from collections import deque
    from datetime import datetime
    from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
    from urllib.parse import urlparse, parse_qs
    
    CAPACIDAD = 14          # mesas por (fecha, turno)
    TURNOS = ("comida", "cena")
    RATE_MAX = 20           # POST /reservas por teléfono...
    RATE_WINDOW = 60.0      # ...por ventana deslizante de 60 s
    
    DB_PATH = os.environ.get(
        "MAREA_DB", os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "reservas.db")
    )
    
    FECHA_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
    
    # ---------------------------------------------------------------------------
    # Persistencia
    # ---------------------------------------------------------------------------
    
    _lock = threading.Lock()       
    …(N2: 16982 chars omitidos en medio)…
    ._send(200, CATALOGO_PRECIOS)
            if len(parts) == 3 and parts[0] == "reservas" and parts[2] == "sugerencias":
                return self._send(*sugerencias_reserva(parts[1]))
            if parts == ["disponibilidad"]:
                fecha = (qs.get("fecha") or [None])[0]
                turno = (qs.get("turno") or [None])[0]
                return self._send(*disponibilidad(fecha, turno))
            if parts == ["reservas"]:
                telefono = (qs.get("telefono") or [None])[0]
                if telefono is None:
                    return self._send(
                        422, {"error": "datos_invalidos", "detalles": ["telefono requerido"]}
                    )
                return self._send(*reservas_por_telefono(telefono))
            if len(parts) == 2 and parts[0] == "reservas":
                with _lock:
                    res = DB.execute("SELECT * FROM reservas WHERE id=?", (parts[1],)).fetchone()
                if res is None:
                    return self._send(404, {"error": "no_encontrada"})
                return self._send(200, to_json(res))
            if parts == ["salud"] or parts == []:
                return self._send(200, {"ok": True, "servicio": "marea-reservas", "version": "v2"})
            self._send(404, {"error": "no_encontrada"})
    
    
    def main():
        addr = ("0.0.0.0", int(os.environ.get("PORT", "8080")))
        httpd = ThreadingHTTPServer(addr, Handler)
        print(f"Marea reservas escuchando en {addr[0]}:{addr[1]} (db: {DB_PATH})", flush=True)
        httpd.serve_forever()
    
    
    if __name__ == "__main__":
        main()

[turno 2] read_file  {"path": "api.md"}
    # Contrato HTTP — v1 (congelado)
    
    Todas las peticiones y respuestas en JSON. La app escucha en `0.0.0.0:8080`.
    Una reserva, allá donde se devuelva, es un objeto con AL MENOS estos campos en el
    **primer nivel** (puedes añadir los que quieras, también en la raíz):
    
    ```
    { "id": str, "nombre": str, "telefono": str, "personas": int,
      "fecha": "YYYY-MM-DD", "turno": "comida"|"cena",
      "estado": "confirmada"|"lista_espera" }
    ```
    
    Reglas de dominio:
    
    - **Durabilidad**: la app puede **reiniciarse en cualquier momento** (deploys,
      cortes). Las reservas deben sobrevivir a un reinicio: persistencia en disco,
      nada de estado solo-en-memoria.
    - Capacidad: **máximo 14 reservas confirmadas** por (fecha, turno). La 15ª y
      siguientes entran en **lista de espera** por orden de llegada (FIFO).
    - `personas` válido: 1 a 6. Fecha válida: `YYYY-MM-DD`. Turno: `comida` o `cena`.
    - Un mismo `telefono` no puede tener DOS reservas activas (confirmada o en espera)
      para la misma (fecha, turno).
    
    ## Endpoints
    
    …(N2: 390 chars omitidos en medio)…
    ntrar)
    - `409` → ese `telefono` ya tiene reserva activa para esa (fecha, turno). El body
      incluye la reserva existente bajo la clave `"reserva"`.
    - `422` → datos inválidos (personas fuera de 1-6, fecha mal formada, turno
      desconocido, campos que faltan). No se crea nada.
    - `429` → límite de peticiones (ver abajo).
    
    ### PATCH /reservas/{id}
    Modifica una reserva activa. Body: cualquier subconjunto de `{personas, fecha, turno}`.
    
    - `200` → la reserva actualizada.
    - `409` → el cambio no cabe (destino lleno) o violaría la regla del teléfono. La
      reserva original queda **exactamente como estaba** (transaccional: nunca a medias,
      nunca perdida).
    - `404` → no existe. · `422` → datos inválidos (original intacta).
    
    ### DELETE /reservas/{id}
    Cancela la reserva. Libera la mesa y, si hay lista de espera para esa (fecha, turno),
    **el primero pasa automáticamente a confirmada**.
    
    - `204` o `200` → cancelada. Un segundo DELETE del mismo id → `404`.
    
    ### GET /disponibilidad?fecha=YYYY-MM-DD&turno=comida|cena
    - `200` → `{"fecha": ..., "turno": ..., "mesas_libres": int}` (0 a 14)
    
    ### GET /reservas?telefono=...
    - `200` → lista (posiblemente vacía) de las reservas ACTIVAS de ese teléfono
      (confirmadas y en lista de espera; las canceladas no aparecen).
    
    ## Límite de peticiones
    
    Máximo **20 POST /reservas por minuto para un mismo `telefono`** (ventana deslizante
    o fija, a tu criterio). Al superarlo → `429`. El límite es por teléfono: peticiones
    de teléfonos distintos NO se bloquean entre sí.

[turno 2] read_file  {"path": "api_v2.md"}
    # Contrato HTTP — v2 (ampliación congelada: precios y sugerencias)
    
    Se AÑADE a la v1 (api.md), que sigue vigente entera. Todos los importes son
    **céntimos de euro, enteros** (int). Nada de floats con dinero.
    
    ## GET /precios
    
    `200` → el catálogo EXACTO de abajo (puedes añadir campos extra, pero estos códigos,
    nombres e importes tienen que estar tal cual):
    
    ```json
    {
      "moneda": "EUR",
      "importes_en": "centimos",
      "menus": [
        {"codigo": "menu-dia",          "nombre": "Menú del día",     "precio_por_persona": 1650},
        {"codigo": "menu-degustacion",  "nombre": "Menú degustación", "precio_por_persona": 4500},
        {"codigo": "menu-grupo",        "nombre": "Menú de grupo",    "precio_por_persona": 2800},
        {"codigo": "menu-san-valentin", "nombre": "Menú San Valentín","precio_por_persona": 5500},
        {"codigo": "menu-nochevieja",   "nombre": "Menú Nochevieja",  "precio_por_persona": 9500}
      ],
      "extras": [
        {"codigo": "maridaje", "nombre": "Maridaje de vinos", "precio_por_persona": 2500}
      ]
    }
    ```
    
    ## GET /reservas/{id}/sugerencias
    
    `200` → lista de ofertas aplicables a ESA reserva (activa). `404` si el id no existe.
    Cada oferta es un objeto con al menos `{"codigo": str}`. Deben salir **exactamente**
    las que apliquen según estas reglas (ni una más, ni una menos; el orden da igual):
    
    1. `menu-grupo` — si la reserva es de **6 personas**.
    2. `menu-san-valentin` — si la fecha es **2027-02-14**.
       `menu-nochevieja` — si la fecha es **2026-12-31**.
    3. `maridaje` — si el turno es **cena** y la fecha cae en **viernes o sábado**.
    4. `descuento-fidelidad` — si ese `telefono` tiene **3 o más reservas activas** en el
       sistema (contando esta). Incluye en la oferta `"descuento_pct": 10`.
    
    Pueden aplicar varias a la vez (se devuelven todas). Si no aplica ninguna → `[]`.

[turno 2] read_file  {"path": "DECISIONS.md"}
    # DECISIONS.md
    
    ## Stack: Python 3 (solo stdlib) + SQLite
    
    - **Python 3 + `http.server`**: la v1 es un CRUD pequeño con reglas de negocio
      claras. La stdlib cubre HTTP, JSON y SQLite sin una sola dependencia externa,
      lo que encaja con el requisito de desplegar **sin salida a internet**: no hay
      `pip install`, no hay supply chain, no hay nada que pueda faltar en producción.
    - **SQLite** como persistencia: fichero local (`data/reservas.db`), transaccional
      (ACID), sobrevive a reinicios y no requiere operar un servidor de BD. Para el
      volumen de un restaurante (14 mesas × 2 turnos) sobra durante años. WAL +
      `synchronous=FULL` para durabilidad ante cortes.
    - **Arquitectura**: un solo proceso, capa HTTP fina (`Handler`) separada de los
      casos de uso (`crear_reserva`, `modificar_reserva`, …), que son funciones puras
      sobre la BD. Si la v2 crece (auth, panel, notificaciones), los casos de uso se
      migran tal cual a FastAPI/Postgres sin tocar la lógica.
    
    ## Cómo se garantizan las reg
    …(N2: 1640 chars omitidos en medio)…
    eño más simple que es *demostrablemente* correcto.
    - **Sin framework**: menos ergonomía a cambio de cero dependencias. El contrato
      v1 está congelado, así que el coste de rutas manuales es bajo y acotado.
    - **`posicion` se calcula al vuelo** (COUNT sobre `seq`), no se almacena: siempre
      es consistente aunque haya promociones o cancelaciones intermedias.
    
    ## v2 — Precios y sugerencias (api_v2.md)
    
    - **Solo lectura, cero migraciones**: los dos endpoints nuevos (`GET /precios`,
      `GET /reservas/{id}/sugerencias`) no tocan el esquema ni escriben nada, así que
      el riesgo sobre las reservas de producción es nulo. Aun así se dejó un backup
      previo en `backups/`.
    - **Catálogo como constante en código** (`CATALOGO_PRECIOS`): el contrato v2 está
      congelado con importes exactos; una tabla añadiría migración y riesgo sin
      aportar nada. Importes SIEMPRE en céntimos enteros (int), nunca floats.
    - **Sugerencias**: reglas puras sobre la reserva + un COUNT de activas por
      teléfono (fidelidad, "contando esta"), todo bajo el mismo `_lock` para leer un
      estado consistente. Maridaje usa `datetime.weekday()` (4=viernes, 5=sábado).
      Reserva cancelada o inexistente → 404 (el contrato habla de reserva "activa").
    - Cada oferta incluye `codigo` (lo exigido) y, si está en catálogo, nombre,
      `precio_por_persona` y `precio_total` — campos extra permitidos por contrato.
    - Verificado: suite v1 íntegra (76 OK) + suite nueva v2 (25 OK) sobre una copia
      de la BD; la BD real quedó bit a bit intacta.

[turno 2] bash  {"command": "cat /workspace/run.sh; echo ---; sqlite3 /workspace/data/reservas.db '.schema' 2>/dev/null; sqlite3 /workspace/data/reservas.db 'select count(*) from reservas' 2>/dev/null"}
    exit=127
    --- stdout ---
    #!/bin/sh
    # Arranca el sistema de reservas "Marea" en 0.0.0.0:8080 (primer plano).
    cd "$(dirname "$0")"
    exec python3 app.py
    ---
    
    --- stderr ---

[turno 3] bash  {"command": "sed -n '1,200p' /workspace/app.py"}
    exit=0
    --- stdout ---
    #!/usr/bin/env python3
    """Sistema de reservas "Marea" — v1.
    
    Stack: Python 3 (stdlib) + SQLite. Sin dependencias externas.
    Contrato HTTP: ver api.md (congelado).
    """
    
    import json
    import os
    import re
    import sqlite3
    import threading
    import time
    import uuid
    from collections import deque
    from datetime import datetime
    from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
    from urllib.parse import urlparse, parse_qs
    
    CAPACIDAD = 14          # mesas por (fecha, turno)
    TURNOS = ("comida", "cena")
    RATE_MAX = 20           # POST /reservas por teléfono...
    RATE_WINDOW = 60.0      # ...por ventana deslizante de 60 s
    
    DB_PATH = os.environ.get(
        "MAREA_DB", os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "reservas.db")
    )
    
    FECHA_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
    
    # ---------------------------------------------------------------------------
    # Persistencia
    # ---------------------------------------------------------------------------
    
    _lock = t
    …(N2: 4046 chars omitidos en medio)…
    
    def valida_str(v) -> bool:
        return isinstance(v, str) and v.strip() != ""
    
    
    def errores_creacion(body) -> list:
        if not isinstance(body, dict):
            return ["body debe ser un objeto JSON"]
        errs = []
        if not valida_str(body.get("nombre")):
            errs.append("nombre: requerido, string no vacío")
        if not valida_str(body.get("telefono")):
            errs.append("telefono: requerido, string no vacío")
        if not valida_personas(body.get("personas")):
            errs.append("personas: entero entre 1 y 6")
        if not valida_fecha(body.get("fecha")):
            errs.append("fecha: formato YYYY-MM-DD válido")
        if not valida_turno(body.get("turno")):
            errs.append("turno: 'comida' o 'cena'")
        return errs
    
    
    # ---------------------------------------------------------------------------
    # Límite de peticiones (por teléfono, ventana deslizante en memoria)
    # ---------------------------------------------------------------------------
    
    
    def rate_limited(telefono: str) -> bool:
        now = time.monotonic()
        with _rate_lock:
            dq = _rate.setdefault(telefono, deque())
            while dq and now - dq[0] >= RATE_WINDOW:
                dq.popleft()
            if len(dq) >= RATE_MAX:
                return True
            dq.append(now)
            return False
    
    
    # ---------------------------------------------------------------------------
    # Casos de uso (cada uno es una transacción bajo _lock)
    # ---------------------------------------------------------------------------
    
    --- stderr ---

[turno 4] bash  {"command": "sed -n '30,120p' /workspace/app.py"}
    exit=0
    --- stdout ---
    
    # ---------------------------------------------------------------------------
    # Persistencia
    # ---------------------------------------------------------------------------
    
    _lock = threading.Lock()          # serializa las transacciones (una cocina, un jefe de sala)
    _rate: dict = {}                  # telefono -> deque[timestamps] (límite de peticiones)
    _rate_lock = threading.Lock()
    
    
    def get_db() -> sqlite3.Connection:
        os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
        conn = sqlite3.connect(DB_PATH, check_same_thread=False)
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA synchronous=FULL")
        conn.execute("PRAGMA foreign_keys=ON")
        conn.executescript(
            """
            CREATE TABLE IF NOT EXISTS reservas (
                seq      INTEGER PRIMARY KEY AUTOINCREMENT,  -- orden de llegada (FIFO)
                id       TEXT UNIQUE NOT NULL,
                nombre   TEXT NOT NULL,
                telefono TEXT
    …(N2: 762 chars omitidos en medio)…
    ------------------------------------
    
    
    def confirmadas(fecha: str, turno: str) -> int:
        row = DB.execute(
            "SELECT COUNT(*) c FROM reservas WHERE fecha=? AND turno=? AND estado='confirmada'",
            (fecha, turno),
        ).fetchone()
        return row["c"]
    
    
    def posicion_espera(res: sqlite3.Row) -> int:
        """Posición 1-based en la lista de espera de su (fecha, turno)."""
        row = DB.execute(
            "SELECT COUNT(*) c FROM reservas "
            "WHERE fecha=? AND turno=? AND estado='lista_espera' AND seq<=?",
            (res["fecha"], res["turno"], res["seq"]),
        ).fetchone()
        return row["c"]
    
    
    def promocionar(fecha: str, turno: str) -> None:
        """Si hay hueco, el primero de la lista de espera pasa a confirmada."""
        if confirmadas(fecha, turno) >= CAPACIDAD:
            return
        row = DB.execute(
            "SELECT seq FROM reservas WHERE fecha=? AND turno=? AND estado='lista_espera' "
            "ORDER BY seq LIMIT 1",
            (fecha, turno),
        ).fetchone()
        if row:
            DB.execute("UPDATE reservas SET estado='confirmada' WHERE seq=?", (row["seq"],))
    
    
    def buscar_activa_por_telefono(telefono: str, fecha: str, turno: str, excluir_id=None):
        q = (
            "SELECT * FROM reservas WHERE telefono=? AND fecha=? AND turno=? "
            "AND estado IN ('confirmada','lista_espera')"
        )
        args = [telefono, fecha, turno]
        if excluir_id is not None:
            q += " AND id<>?"
            args.append(excluir_id)
        return DB.execute(q, args).fetchone()
    
    
    --- stderr ---

[turno 4] bash  {"command": "grep -n \"\" /workspace/app.py | sed -n '200,400p'"}
    exit=0
    --- stdout ---
    200:# ---------------------------------------------------------------------------
    201:
    202:
    203:def crear_reserva(body, idem_key):
    204:    errs = errores_creacion(body)
    205:    if errs:
    206:        return 422, {"error": "datos_invalidos", "detalles": errs}
    207:
    208:    telefono = body["telefono"].strip()
    209:
    210:    with _lock:
    211:        # Idempotencia: misma clave => misma reserva, nunca dos.
    212:        if idem_key:
    213:            row = DB.execute(
    214:                "SELECT reserva_id, status FROM idempotencia WHERE clave=?", (idem_key,)
    215:            ).fetchone()
    216:            if row:
    217:                res = DB.execute(
    218:                    "SELECT * FROM reservas WHERE id=?", (row["reserva_id"],)
    219:                ).fetchone()
    220:                if res is not None:
    221:                    status = 201 if res["estado"] == "confirmada" else (
    222:                        202 if res["estado"] == "lista_espera" else 200
    223:                    )
    2
    …(N2: 6441 chars omitidos en medio)…
    :    ],
    362:    "extras": [
    363:        {"codigo": "maridaje", "nombre": "Maridaje de vinos", "precio_por_persona": 2500},
    364:    ],
    365:}
    366:
    367:_PRECIO_POR_CODIGO = {
    368:    e["codigo"]: e["precio_por_persona"]
    369:    for e in CATALOGO_PRECIOS["menus"] + CATALOGO_PRECIOS["extras"]
    370:}
    371:_NOMBRE_POR_CODIGO = {
    372:    e["codigo"]: e["nombre"]
    373:    for e in CATALOGO_PRECIOS["menus"] + CATALOGO_PRECIOS["extras"]
    374:}
    375:
    376:
    377:def _oferta(codigo: str, personas: int, **extra) -> dict:
    378:    d = {"codigo": codigo}
    379:    if codigo in _NOMBRE_POR_CODIGO:
    380:        d["nombre"] = _NOMBRE_POR_CODIGO[codigo]
    381:        d["precio_por_persona"] = _PRECIO_POR_CODIGO[codigo]
    382:        d["precio_total"] = _PRECIO_POR_CODIGO[codigo] * personas
    383:    d.update(extra)
    384:    return d
    385:
    386:
    387:def sugerencias_reserva(rid: str):
    388:    """Ofertas aplicables a una reserva activa según api_v2.md."""
    389:    with _lock:
    390:        res = DB.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone()
    391:        if res is None or res["estado"] == "cancelada":
    392:            return 404, {"error": "no_encontrada"}
    393:        activas_tel = DB.execute(
    394:            "SELECT COUNT(*) c FROM reservas "
    395:            "WHERE telefono=? AND estado IN ('confirmada','lista_espera')",
    396:            (res["telefono"],),
    397:        ).fetchone()["c"]
    398:
    399:    personas, fecha, turno = res["personas"], res["fecha"], res["turno"]
    400:    ofertas = []
    
    --- stderr ---

[turno 5] bash  {"command": "sed -n '55,90p' /workspace/app.py; echo =====; sed -n '223,360p' /workspace/app.py"}
    exit=0
    --- stdout ---
                fecha    TEXT NOT NULL,
                turno    TEXT NOT NULL,
                estado   TEXT NOT NULL CHECK (estado IN ('confirmada','lista_espera','cancelada'))
            );
            CREATE INDEX IF NOT EXISTS idx_slot ON reservas (fecha, turno, estado);
            CREATE INDEX IF NOT EXISTS idx_tel  ON reservas (telefono, estado);
            CREATE TABLE IF NOT EXISTS idempotencia (
                clave      TEXT PRIMARY KEY,
                reserva_id TEXT NOT NULL,
                status     INTEGER NOT NULL
            );
            """
        )
        conn.commit()
        return conn
    
    
    DB = get_db()
    
    # ---------------------------------------------------------------------------
    # Dominio
    # ---------------------------------------------------------------------------
    
    
    def confirmadas(fecha: str, turno: str) -> int:
        row = DB.execute(
            "SELECT COUNT(*) c FROM reservas WHERE fecha=? AND turno=? AND estado='confirmada'",
            (fecha, turno),
        ).fetchone()
        return row["c"]
    
    
    d
    …(N2: 4616 chars omitidos en medio)…
    return 204, None
    
    
    def disponibilidad(fecha, turno):
        if not valida_fecha(fecha) or not valida_turno(turno):
            return 422, {"error": "datos_invalidos", "detalles": ["fecha y turno válidos requeridos"]}
        with _lock:
            libres = CAPACIDAD - confirmadas(fecha, turno)
        return 200, {"fecha": fecha, "turno": turno, "mesas_libres": max(0, libres)}
    
    
    def reservas_por_telefono(telefono):
        with _lock:
            rows = DB.execute(
                "SELECT * FROM reservas WHERE telefono=? AND estado IN ('confirmada','lista_espera') "
                "ORDER BY seq",
                (telefono,),
            ).fetchall()
            return 200, [to_json(r) for r in rows]
    
    
    
    # ---------------------------------------------------------------------------
    # v2 — Catálogo de precios y sugerencias (api_v2.md). Importes en céntimos.
    # ---------------------------------------------------------------------------
    
    CATALOGO_PRECIOS = {
        "moneda": "EUR",
        "importes_en": "centimos",
        "menus": [
            {"codigo": "menu-dia",          "nombre": "Menú del día",     "precio_por_persona": 1650},
            {"codigo": "menu-degustacion",  "nombre": "Menú degustación", "precio_por_persona": 4500},
            {"codigo": "menu-grupo",        "nombre": "Menú de grupo",    "precio_por_persona": 2800},
            {"codigo": "menu-san-valentin", "nombre": "Menú San Valentín","precio_por_persona": 5500},
            {"codigo": "menu-nochevieja",   "nombre": "Menú Nochevieja",  "precio_por_persona": 9500},
    
    --- stderr ---

[turno 6] bash  {"command": "sed -n '223,330p' /workspace/app.py"}
    exit=0
    --- stdout ---
                        )
                        return status, to_json(res)
    
            if rate_limited(telefono):
                return 429, {"error": "rate_limit", "detalle": "máximo 20 POST/minuto por teléfono"}
    
            existente = buscar_activa_por_telefono(telefono, body["fecha"], body["turno"])
            if existente:
                return 409, {
                    "error": "telefono_duplicado",
                    "detalle": "ese teléfono ya tiene una reserva activa para esa fecha y turno",
                    "reserva": to_json(existente),
                }
    
            estado = (
                "confirmada"
                if confirmadas(body["fecha"], body["turno"]) < CAPACIDAD
                else "lista_espera"
            )
            rid = str(uuid.uuid4())
            DB.execute(
                "INSERT INTO reservas (id, nombre, telefono, personas, fecha, turno, estado) "
                "VALUES (?,?,?,?,?,?,?)",
                (rid, body["nombre"].strip(), telefono, body["personas"], body["fecha"],
                 bo
    …(N2: 2079 chars omitidos en medio)…
    ono_duplicado",
                    "detalle": "el teléfono ya tiene una reserva activa en el destino",
                    "reserva": to_json(otra),
                }
            if confirmadas(nueva_fecha, nuevo_turno) >= CAPACIDAD:
                return 409, {
                    "error": "sin_capacidad",
                    "detalle": "el turno de destino está completo; la reserva original no ha cambiado",
                }
    
            liberaba_mesa = res["estado"] == "confirmada"
            origen = (res["fecha"], res["turno"])
            DB.execute(
                "UPDATE reservas SET personas=?, fecha=?, turno=?, estado='confirmada' WHERE id=?",
                (nuevas_personas, nueva_fecha, nuevo_turno, rid),
            )
            if liberaba_mesa:
                promocionar(*origen)  # su antigua mesa entra en juego
            DB.commit()
            return 200, to_json(DB.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone())
    
    
    def cancelar_reserva(rid):
        with _lock:
            res = DB.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone()
            if res is None or res["estado"] == "cancelada":
                return 404, {"error": "no_encontrada"}
            liberaba_mesa = res["estado"] == "confirmada"
            DB.execute("UPDATE reservas SET estado='cancelada' WHERE id=?", (rid,))
            if liberaba_mesa:
                promocionar(res["fecha"], res["turno"])
            DB.commit()
            return 204, None
    
    
    def disponibilidad(fecha, turno):
        if not valida_fecha(fecha) or not valida_turno(turno):
    
    --- stderr ---

[turno 7] bash  {"command": "sed -n '244,300p' /workspace/app.py"}
    exit=0
    --- stdout ---
                "INSERT INTO reservas (id, nombre, telefono, personas, fecha, turno, estado) "
                "VALUES (?,?,?,?,?,?,?)",
                (rid, body["nombre"].strip(), telefono, body["personas"], body["fecha"],
                 body["turno"], estado),
            )
            status = 201 if estado == "confirmada" else 202
            if idem_key:
                DB.execute(
                    "INSERT OR IGNORE INTO idempotencia (clave, reserva_id, status) VALUES (?,?,?)",
                    (idem_key, rid, status),
                )
            DB.commit()
            res = DB.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone()
            return status, to_json(res)
    
    
    def modificar_reserva(rid, body):
        if not isinstance(body, dict):
            return 422, {"error": "datos_invalidos", "detalles": ["body debe ser un objeto JSON"]}
    
        errs = []
        if "personas" in body and not valida_personas(body["personas"]):
            errs.append("personas: entero entre 1 y 6")
        if "fecha" in body an
    …(N2: 133 chars omitidos en medio)…
    o(body["turno"]):
            errs.append("turno: 'comida' o 'cena'")
        if errs:
            return 422, {"error": "datos_invalidos", "detalles": errs}
    
        with _lock:
            res = DB.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone()
            if res is None or res["estado"] == "cancelada":
                return 404, {"error": "no_encontrada"}
    
            nueva_fecha = body.get("fecha", res["fecha"])
            nuevo_turno = body.get("turno", res["turno"])
            nuevas_personas = body.get("personas", res["personas"])
            cambia_slot = (nueva_fecha, nuevo_turno) != (res["fecha"], res["turno"])
    
            if not cambia_slot:
                # Solo cambian personas (o nada): la mesa no varía, estado intacto.
                DB.execute("UPDATE reservas SET personas=? WHERE id=?", (nuevas_personas, rid))
                DB.commit()
                return 200, to_json(DB.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone())
    
            # Cambio de (fecha, turno): comprobar regla del teléfono y capacidad. Todo o nada.
            otra = buscar_activa_por_telefono(res["telefono"], nueva_fecha, nuevo_turno, excluir_id=rid)
            if otra:
                return 409, {
                    "error": "telefono_duplicado",
                    "detalle": "el teléfono ya tiene una reserva activa en el destino",
                    "reserva": to_json(otra),
                }
            if confirmadas(nueva_fecha, nuevo_turno) >= CAPACIDAD:
                return 409, {
                    "error": "sin_capacidad",
    
    --- stderr ---

[turno 7] bash  {"command": "sed -n '400,520p' /workspace/app.py"}
    exit=0
    --- stdout ---
        ofertas = []
    
        # 1. Menú de grupo: reserva de 6 personas.
        if personas == 6:
            ofertas.append(_oferta("menu-grupo", personas))
    
        # 2. Fechas especiales.
        if fecha == "2027-02-14":
            ofertas.append(_oferta("menu-san-valentin", personas))
        if fecha == "2026-12-31":
            ofertas.append(_oferta("menu-nochevieja", personas))
    
        # 3. Maridaje: cena en viernes o sábado.
        if turno == "cena" and datetime.strptime(fecha, "%Y-%m-%d").weekday() in (4, 5):
            ofertas.append(_oferta("maridaje", personas))
    
        # 4. Fidelidad: 3+ reservas activas de ese teléfono (contando esta).
        if activas_tel >= 3:
            ofertas.append({"codigo": "descuento-fidelidad", "descuento_pct": 10})
    
        return 200, ofertas
    
    
    # ---------------------------------------------------------------------------
    # HTTP
    # ---------------------------------------------------------------------------
    
    
    class Handler(BaseHTTPRequestHandler):
        protocol_version = "HTTP
    …(N2: 2442 chars omitidos en medio)…
      if parts == ["precios"]:
                return self._send(200, CATALOGO_PRECIOS)
            if len(parts) == 3 and parts[0] == "reservas" and parts[2] == "sugerencias":
                return self._send(*sugerencias_reserva(parts[1]))
            if parts == ["disponibilidad"]:
                fecha = (qs.get("fecha") or [None])[0]
                turno = (qs.get("turno") or [None])[0]
                return self._send(*disponibilidad(fecha, turno))
            if parts == ["reservas"]:
                telefono = (qs.get("telefono") or [None])[0]
                if telefono is None:
                    return self._send(
                        422, {"error": "datos_invalidos", "detalles": ["telefono requerido"]}
                    )
                return self._send(*reservas_por_telefono(telefono))
            if len(parts) == 2 and parts[0] == "reservas":
                with _lock:
                    res = DB.execute("SELECT * FROM reservas WHERE id=?", (parts[1],)).fetchone()
                if res is None:
                    return self._send(404, {"error": "no_encontrada"})
                return self._send(200, to_json(res))
            if parts == ["salud"] or parts == []:
                return self._send(200, {"ok": True, "servicio": "marea-reservas", "version": "v2"})
            self._send(404, {"error": "no_encontrada"})
    
    
    def main():
        addr = ("0.0.0.0", int(os.environ.get("PORT", "8080")))
        httpd = ThreadingHTTPServer(addr, Handler)
        print(f"Marea reservas escuchando en {addr[0]}:{addr[1]} (db: {DB_PATH})", flush=True)
    
    --- stderr ---

[turno 8] bash  {"command": "head -40 /workspace/test_api.py; grep -n \"MAREA_DB\\|8080\\|subprocess\\|localhost\" /workspace/test_api.py /workspace/test_api_v2.py | head"}
    exit=0
    --- stdout ---
    #!/usr/bin/env python3
    """Pruebas de integración del contrato v1 contra localhost:8080."""
    import json
    import subprocess
    import sys
    import time
    import urllib.request
    import urllib.error
    
    BASE = "http://localhost:8080"
    PASS = FAIL = 0
    
    
    def req(method, path, body=None, headers=None):
        data = json.dumps(body).encode() if body is not None else None
        r = urllib.request.Request(BASE + path, data=data, method=method,
                                   headers=headers or {})
        if data:
            r.add_header("Content-Type", "application/json")
        try:
            with urllib.request.urlopen(r) as resp:
                raw = resp.read()
                return resp.status, json.loads(raw) if raw else None
        except urllib.error.HTTPError as e:
            raw = e.read()
            return e.code, (json.loads(raw) if raw else None)
    
    
    def check(desc, expected, got):
        global PASS, FAIL
        if expected == got:
            PASS += 1
        else:
            FAIL += 1
            print(f"FAIL: {desc} (esperado {expected!r}, obtenido {got!r})")
    
    
    # --- validación 422 ---
    for body, desc in [
        ({"nombre": "A", "telefono": "t1", "personas": 7, "fecha": "2025-12-01", "turno": "comida"}, "personas>6"),
    /workspace/test_api.py:2:"""Pruebas de integración del contrato v1 contra localhost:8080."""
    /workspace/test_api.py:4:import subprocess
    /workspace/test_api.py:10:BASE = "http://localhost:8080"
    /workspace/test_api.py:182:subprocess.Popen(["sh", "run.sh"], stdout=open("/tmp/app2.log", "w"),
    /workspace/test_api.py:183:                 stderr=subprocess.STDOUT, cwd=".")
    /workspace/test_api_v2.py:2:"""Pruebas de integración del contrato v2 (precios y sugerencias) contra localhost:8080."""
    /workspace/test_api_v2.py:7:BASE = "http://localhost:8080"
    
    --- stderr ---

[turno 8] bash  {"command": "sqlite3 /workspace/data/reservas.db 'select count(*), estado from reservas group by estado' 2>/dev/null || python3 -c \"\nimport sqlite3; c=sqlite3.connect('/workspace/data/reservas.db')\nprint(c.execute('select estado,count(*) from reservas group by estado').fetchall())\nprint(c.execute('select count(*) from reservas').fetchall())\""}
    exit=0
    --- stdout ---
    [('cancelada', 7), ('confirmada', 15260), ('lista_espera', 8)]
    [(15275,)]
    
    --- stderr ---

[turno 9] bash  {"command": "python3 - <<'EOF'\nimport sqlite3\nc = sqlite3.connect('/workspace/data/reservas.db')\n# ¿algún slot con >14 confirmadas?\nprint(\"slots >14:\", c.execute(\"select fecha,turno,count(*) c from reservas where estado='confirmada' group by 1,2 having c>14\").fetchall()[:5])\n# ¿violaciones de la regla del teléfono?\nprint(\"tel dup:\", c.execute(\"select telefono,fecha,turno,count(*) c from reservas where estado in ('confirmada','lista_espera') group by 1,2,3 having c>1\").fetchall()[:5])\nprint(\"num slots:\", c.execute(\"select count(*) from (select distinct fecha,turno from reservas)\").fetchone())\nEOF"}
    exit=0
    --- stdout ---
    slots >14: []
    tel dup: []
    num slots: (1179,)
    
    --- stderr ---

[turno 9] bash  {"command": "cp /workspace/data/reservas.db /workspace/backups/reservas.db.bak.pre-v3 2>/dev/null; ls /workspace/backups/; python3 --version"}
    exit=0
    --- stdout ---
    reservas.db.bak.1783945283
    reservas.db.bak.pre-v3
    Python 3.12.13
    
    --- stderr ---

[turno 10] bash  {"command": "sed -n '425,490p' /workspace/app.py"}
    exit=0
    --- stdout ---
    # ---------------------------------------------------------------------------
    
    
    class Handler(BaseHTTPRequestHandler):
        protocol_version = "HTTP/1.1"
        server_version = "Marea/1.0"
    
        def log_message(self, fmt, *args):  # silencio en producción
            pass
    
        # -- helpers -----------------------------------------------------------
        def _send(self, status, payload):
            body = b"" if payload is None else json.dumps(payload, ensure_ascii=False).encode()
            self.send_response(status)
            if body:
                self.send_header("Content-Type", "application/json; charset=utf-8")
            self.send_header("Content-Length", str(len(body)))
            self.end_headers()
            if body:
                self.wfile.write(body)
    
        def _json_body(self):
            try:
                n = int(self.headers.get("Content-Length") or 0)
            except ValueError:
                return None, False
            raw = self.rfile.read(n) if n else b""
            if not raw:
             
    …(N2: 121 chars omitidos en medio)…
    ode("utf-8")), True
            except (ValueError, UnicodeDecodeError):
                return None, False
    
        def _path_parts(self):
            p = urlparse(self.path)
            return [s for s in p.path.split("/") if s], parse_qs(p.query)
    
        # -- métodos -----------------------------------------------------------
        def do_POST(self):
            parts, _ = self._path_parts()
            if parts == ["reservas"]:
                body, ok = self._json_body()
                if not ok:
                    return self._send(422, {"error": "datos_invalidos", "detalles": ["JSON malformado"]})
                idem = self.headers.get("Idempotency-Key")
                return self._send(*crear_reserva(body if body is not None else {}, idem))
            self._send(404, {"error": "no_encontrada"})
    
        def do_PATCH(self):
            parts, _ = self._path_parts()
            if len(parts) == 2 and parts[0] == "reservas":
                body, ok = self._json_body()
                if not ok:
                    return self._send(422, {"error": "datos_invalidos", "detalles": ["JSON malformado"]})
                return self._send(*modificar_reserva(parts[1], body if body is not None else {}))
            self._send(404, {"error": "no_encontrada"})
    
        def do_DELETE(self):
            parts, _ = self._path_parts()
            if len(parts) == 2 and parts[0] == "reservas":
                return self._send(*cancelar_reserva(parts[1]))
            self._send(404, {"error": "no_encontrada"})
    
        def do_GET(self):
            parts, qs = self._path_parts()
    
    --- stderr ---

[turno 11] bash  {"command": "sed -n '40,110p' /workspace/test_api.py"}
    exit=0
    --- stdout ---
        ({"nombre": "A", "telefono": "t1", "personas": 7, "fecha": "2025-12-01", "turno": "comida"}, "personas>6"),
        ({"nombre": "A", "telefono": "t1", "personas": 0, "fecha": "2025-12-01", "turno": "comida"}, "personas<1"),
        ({"nombre": "A", "telefono": "t1", "personas": 2, "fecha": "2025-13-01", "turno": "comida"}, "fecha invalida"),
        ({"nombre": "A", "telefono": "t1", "personas": 2, "fecha": "2025-12-1", "turno": "comida"}, "fecha mal formada"),
        ({"nombre": "A", "telefono": "t1", "personas": 2, "fecha": "2025-12-01", "turno": "brunch"}, "turno desconocido"),
        ({"telefono": "t1", "personas": 2, "fecha": "2025-12-01", "turno": "comida"}, "falta nombre"),
        ({"nombre": "A", "personas": 2, "fecha": "2025-12-01", "turno": "cena"}, "falta telefono"),
    ]:
        s, _ = req("POST", "/reservas", body)
        check(f"422 {desc}", 422, s)
    
    FECHA = "2025-12-01"
    
    # --- capacidad: 14 confirmadas, 15ª y 16ª a lista de espera ---
    ids = []
    for i in range(14):
        s, b = 
    …(N2: 1283 chars omitidos en medio)…
    "409 incluye reserva", ids[0], b["reserva"]["id"])
    # mismo teléfono, otro turno: permitido
    s, b = req("POST", "/reservas", {"nombre": "C0", "telefono": "tel-0",
                                     "personas": 2, "fecha": FECHA, "turno": "comida"})
    check("mismo tel otro turno => 201", 201, s)
    req("DELETE", f"/reservas/{b['id']}")
    # dedup también con lista de espera
    s, b = req("POST", "/reservas", {"nombre": "W1bis", "telefono": "tel-w1",
                                     "personas": 2, "fecha": FECHA, "turno": "cena"})
    check("dup en espera => 409", 409, s)
    
    # --- idempotencia ---
    key = "clave-abc-123"
    s1, b1 = req("POST", "/reservas", {"nombre": "I", "telefono": "tel-idem", "personas": 2,
                                       "fecha": "2025-12-02", "turno": "comida"},
                 {"Idempotency-Key": key})
    s2, b2 = req("POST", "/reservas", {"nombre": "I", "telefono": "tel-idem", "personas": 2,
                                       "fecha": "2025-12-02", "turno": "comida"},
                 {"Idempotency-Key": key})
    check("idem 1a => 201", 201, s1)
    check("idem replay mismo id", b1["id"], b2["id"])
    s, lst = req("GET", "/reservas?telefono=tel-idem")
    check("idem: solo UNA reserva", 1, len(lst))
    
    # --- GET /reservas?telefono ---
    s, lst = req("GET", "/reservas?telefono=tel-w1")
    check("activas de tel-w1", 1, len(lst))
    check("estado espera en listado", "lista_espera", lst[0]["estado"])
    s, lst = req("GET", "/reservas?telefono=no-existe")
    check("telefono sin reservas => []", [], lst)
    
    --- stderr ---

[turno 11] bash  {"command": "sed -n '110,230p' /workspace/test_api.py; echo ====; sed -n '1,60p' /workspace/test_api_v2.py"}
    exit=0
    --- stdout ---
    check("telefono sin reservas => []", [], lst)
    
    # --- PATCH ---
    # mover una confirmada a turno lleno => 409 y original intacta
    s, b = req("PATCH", f"/reservas/{ids[1]}", {"turno": "cena", "fecha": FECHA})
    check("patch no-op mismo slot => 200", 200, s)
    sX, bX = req("POST", "/reservas", {"nombre": "X", "telefono": "tel-x", "personas": 2,
                                       "fecha": "2025-12-03", "turno": "comida"})
    s, b = req("PATCH", f"/reservas/{bX['id']}", {"fecha": FECHA, "turno": "cena"})
    check("patch a turno lleno => 409", 409, s)
    s, b = req("GET", f"/reservas/{bX['id']}")
    check("original intacta fecha", "2025-12-03", b["fecha"])
    check("original intacta estado", "confirmada", b["estado"])
    # patch personas inválidas => 422, intacta
    s, _ = req("PATCH", f"/reservas/{bX['id']}", {"personas": 9})
    check("patch personas invalidas => 422", 422, s)
    s, b = req("PATCH", f"/reservas/{bX['id']}", {"personas": 6})
    check("patch personas => 200", 200, s)
    check("personas actuali
    …(N2: 3524 chars omitidos en medio)…
    d)
        if data:
            r.add_header("Content-Type", "application/json")
        try:
            with urllib.request.urlopen(r) as resp:
                raw = resp.read()
                return resp.status, json.loads(raw) if raw else None
        except urllib.error.HTTPError as e:
            raw = e.read()
            return e.code, (json.loads(raw) if raw else None)
    
    
    def check(desc, expected, got):
        global PASS, FAIL
        if expected == got:
            PASS += 1
            print(f"  OK  {desc}")
        else:
            FAIL += 1
            print(f" FAIL {desc}: esperaba {expected!r}, obtuve {got!r}")
    
    
    def crear(tel, personas, fecha, turno, nombre="Test V2"):
        s, b = req("POST", "/reservas", {
            "nombre": nombre, "telefono": tel, "personas": personas,
            "fecha": fecha, "turno": turno,
        })
        assert s in (201, 202), (s, b)
        return b["id"]
    
    
    def codigos(rid):
        s, b = req("GET", f"/reservas/{rid}/sugerencias")
        assert s == 200, (s, b)
        return sorted(o["codigo"] for o in b), b
    
    
    # ---- GET /precios ---------------------------------------------------------
    s, b = req("GET", "/precios")
    check("GET /precios status", 200, s)
    check("moneda", "EUR", b.get("moneda"))
    check("importes_en", "centimos", b.get("importes_en"))
    esperado_menus = {
        "menu-dia": ("Menú del día", 1650),
        "menu-degustacion": ("Menú degustación", 4500),
        "menu-grupo": ("Menú de grupo", 2800),
        "menu-san-valentin": ("Menú San Valentín", 5500),
        "menu-nochevieja": ("Menú Nochevieja", 9500),
    
    --- stderr ---

[turno 12] bash  {"command": "sed -n '140,215p' /workspace/test_api.py"}
    exit=0
    --- stdout ---
    check("delete => 204", 204, s)
    s, _ = req("DELETE", f"/reservas/{ids[0]}")
    check("segundo delete => 404", 404, s)
    s, b = req("GET", f"/reservas/{w1['id']}")
    check("W1 promocionada", "confirmada", b["estado"])
    s, b = req("GET", f"/reservas/{w2['id']}")
    check("W2 sigue en espera", "lista_espera", b["estado"])
    check("W2 ahora posicion 1", 1, b["posicion"])
    s, b = req("GET", f"/disponibilidad?fecha={FECHA}&turno=cena")
    check("sigue llena tras promocion", 0, b["mesas_libres"])
    # mover confirmada fuera promociona a W2
    s, b = req("PATCH", f"/reservas/{ids[2]}", {"fecha": "2025-12-05"})
    check("patch mueve de turno => 200", 200, s)
    s, b = req("GET", f"/reservas/{w2['id']}")
    check("W2 promocionada tras patch", "confirmada", b["estado"])
    
    # --- rate limit: 20 POST/min por teléfono ---
    codes = []
    for i in range(22):
        s, _ = req("POST", "/reservas", {"nombre": "R", "telefono": "tel-rl", "personas": 2,
                                         "fecha": "2026-01-01", "turno": "com
    …(N2: 115 chars omitidos en medio)…
    codes[1:20]))
    check("21a y 22a => 429", {429}, set(codes[20:]))
    # otro teléfono NO bloqueado
    s, _ = req("POST", "/reservas", {"nombre": "R2", "telefono": "tel-rl-2", "personas": 2,
                                     "fecha": "2026-01-01", "turno": "comida"})
    check("otro telefono no bloqueado", 201, s)
    
    # --- durabilidad: reiniciar la app y comprobar que todo sigue ---
    print("Reiniciando app para probar durabilidad...")
    import os, signal
    for pid in os.listdir("/proc"):
        if pid.isdigit() and pid != str(os.getpid()):
            try:
                cmd = open(f"/proc/{pid}/cmdline","rb").read().decode(errors="ignore")
                if "app.py" in cmd.split("\x00"):
                    os.kill(int(pid), signal.SIGTERM)
            except (OSError, IOError):
                pass
    time.sleep(1)
    subprocess.Popen(["sh", "run.sh"], stdout=open("/tmp/app2.log", "w"),
                     stderr=subprocess.STDOUT, cwd=".")
    time.sleep(1.5)
    s, b = req("GET", f"/reservas/{w2['id']}")
    check("tras reinicio: reserva sigue", "confirmada", b["estado"])
    s, b = req("GET", f"/disponibilidad?fecha={FECHA}&turno=cena")
    check("tras reinicio: disponibilidad", 0, b["mesas_libres"])
    s2, b2 = req("POST", "/reservas", {"nombre": "I", "telefono": "tel-idem", "personas": 2,
                                       "fecha": "2025-12-02", "turno": "comida"},
                 {"Idempotency-Key": key})
    check("tras reinicio: idempotencia persiste", b1["id"], b2["id"])
    
    print(f"\n{PASS} OK, {FAIL} FAIL")
    sys.exit(1 if FAIL else 0)
    
    --- stderr ---

[turno 13] bash  {"command": "grep -n \"def to_json\" -A 15 /workspace/app.py; grep -n \"def valida_fecha\" -A 10 /workspace/app.py"}
    exit=0
    --- stdout ---
    122:def to_json(res: sqlite3.Row, con_posicion: bool = True) -> dict:
    123-    d = {
    124-        "id": res["id"],
    125-        "nombre": res["nombre"],
    126-        "telefono": res["telefono"],
    127-        "personas": res["personas"],
    128-        "fecha": res["fecha"],
    129-        "turno": res["turno"],
    130-        "estado": res["estado"],
    131-    }
    132-    if con_posicion and res["estado"] == "lista_espera":
    133-        d["posicion"] = posicion_espera(res)
    134-    return d
    135-
    136-
    137-# ---------------------------------------------------------------------------
    142:def valida_fecha(v) -> bool:
    143-    if not isinstance(v, str) or not FECHA_RE.match(v):
    144-        return False
    145-    try:
    146-        datetime.strptime(v, "%Y-%m-%d")
    147-        return True
    148-    except ValueError:
    149-        return False
    150-
    151-
    152-def valida_personas(v) -> bool:
    
    --- stderr ---

[turno 14] write_file  {"path": "app.py", "content": "<<24884 bytes escritos; cuerpo omitido del historial (N1); usa read_file para releerlo>>"}
    ok

[turno 15] bash  {"command": "wc -c /workspace/app.py && python3 -m py_compile /workspace/app.py && echo COMPILA && head -60 /workspace/app.py"}
    exit=0
    --- stdout ---
    24884 /workspace/app.py
    COMPILA
    #!/usr/bin/env python3
    """Sistema de reservas "Marea" — v3 (endurecido para concurrencia).
    
    Stack: Python 3 (stdlib) + SQLite. Sin dependencias externas.
    Contrato HTTP: api.md (v1) + api_v2.md (v2), ambos congelados.
    
    Cambios v3 (rendimiento y corrección bajo concurrencia):
      - Conexión SQLite POR HILO + WAL: las lecturas (disponibilidad, listados,
        sugerencias) van en paralelo, sin lock global, con snapshot consistente.
      - Las escrituras se serializan con un lock de escritura + BEGIN IMMEDIATE:
        transaccionales, todo-o-nada. Nunca >14 confirmadas por (fecha, turno).
      - Índice ÚNICO parcial en BD (telefono, fecha, turno WHERE activa): la regla
        del teléfono queda garantizada también a nivel de motor (defensa en
        profundidad), no solo por la lógica de aplicación.
      - Rate limiter con poda periódica (memoria acotada).
      - Servidor: cola de accept amplia, hilos daemon, timeout de socket.
    """
    
    import json
    import os
    import re
    import sqlite3
    import threading
    import time
    import uuid
    from collections import deque
    from contextlib import contextmanager
    from datetime import datetime
    from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
    from urllib.parse import urlparse, parse_qs
    
    CAPACIDAD = 14          # mesas por (fecha, turno)
    TURNOS = ("comida", "cena")
    RATE_MAX = 20           # POST /reservas por teléfono...
    RATE_WINDOW = 60.0      # ...por ventana deslizante de 60 s
    
    DB_PATH = os.environ.get(
        "MAREA_DB", os.path.join(os.path.dirname(os.path.abspath(__file__)), "data", "reservas.db")
    )
    
    FECHA_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$")
    
    # ---------------------------------------------------------------------------
    # Persistencia: una conexión por hilo (WAL => lectores paralelos, un escritor)
    # ---------------------------------------------------------------------------
    
    _write_lock = threading.Lock()   # serializa transacciones de escritura
    _local = threading.local()       # conexión SQLite por hilo
    
    
    def _connect() -> sqlite3.Connection:
        conn = sqlite3.connect(DB_PATH, timeout=30.0)
        conn.row_factory = sqlite3.Row
        conn.execute("PRAGMA journal_mode=WAL")
        conn.execute("PRAGMA synchronous=FULL")   # durabilidad ante cortes
        conn.execute("PRAGMA busy_timeout=30000")
        conn.execute("PRAGMA foreign_keys=ON")
        return conn
    
    
    
    --- stderr ---

[turno 16] bash  {"command": "sed -n '60,240p' /workspace/app.py"}
    exit=0
    --- stdout ---
    
    def db() -> sqlite3.Connection:
        conn = getattr(_local, "conn", None)
        if conn is None:
            conn = _local.conn = _connect()
        return conn
    
    
    def init_db() -> None:
        os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
        conn = _connect()
        conn.executescript(
            """
            CREATE TABLE IF NOT EXISTS reservas (
                seq      INTEGER PRIMARY KEY AUTOINCREMENT,  -- orden de llegada (FIFO)
                id       TEXT UNIQUE NOT NULL,
                nombre   TEXT NOT NULL,
                telefono TEXT NOT NULL,
                personas INTEGER NOT NULL CHECK (personas BETWEEN 1 AND 6),
                fecha    TEXT NOT NULL,
                turno    TEXT NOT NULL,
                estado   TEXT NOT NULL CHECK (estado IN ('confirmada','lista_espera','cancelada'))
            );
            CREATE INDEX IF NOT EXISTS idx_slot ON reservas (fecha, turno, estado);
            CREATE INDEX IF NOT EXISTS idx_tel  ON reservas (telefono, estado);
            CREATE TABLE IF NOT EXISTS i
    …(N2: 3364 chars omitidos en medio)…
      if con_posicion and res["estado"] == "lista_espera":
            d["posicion"] = posicion_espera(conn, res)
        return d
    
    
    # ---------------------------------------------------------------------------
    # Validación
    # ---------------------------------------------------------------------------
    
    
    def valida_fecha(v) -> bool:
        if not isinstance(v, str) or not FECHA_RE.match(v):
            return False
        try:
            datetime.strptime(v, "%Y-%m-%d")
            return True
        except ValueError:
            return False
    
    
    def valida_personas(v) -> bool:
        return isinstance(v, int) and not isinstance(v, bool) and 1 <= v <= 6
    
    
    def valida_turno(v) -> bool:
        return v in TURNOS
    
    
    def valida_str(v) -> bool:
        return isinstance(v, str) and v.strip() != ""
    
    
    def errores_creacion(body) -> list:
        if not isinstance(body, dict):
            return ["body debe ser un objeto JSON"]
        errs = []
        if not valida_str(body.get("nombre")):
            errs.append("nombre: requerido, string no vacío")
        if not valida_str(body.get("telefono")):
            errs.append("telefono: requerido, string no vacío")
        if not valida_personas(body.get("personas")):
            errs.append("personas: entero entre 1 y 6")
        if not valida_fecha(body.get("fecha")):
            errs.append("fecha: formato YYYY-MM-DD válido")
        if not valida_turno(body.get("turno")):
            errs.append("turno: 'comida' o 'cena'")
        return errs
    
    
    # ---------------------------------------------------------------------------
    
    --- stderr ---

[turno 17] bash  {"command": "sed -n '240,430p' /workspace/app.py"}
    exit=0
    --- stdout ---
    # ---------------------------------------------------------------------------
    # Límite de peticiones (por teléfono, ventana deslizante en memoria, acotada)
    # ---------------------------------------------------------------------------
    
    _rate: dict = {}                 # telefono -> deque[timestamps]
    _rate_lock = threading.Lock()
    _rate_last_gc = 0.0
    
    
    def rate_limited(telefono: str) -> bool:
        global _rate_last_gc
        now = time.monotonic()
        with _rate_lock:
            # Poda periódica: teléfonos sin actividad en la ventana no ocupan memoria.
            if now - _rate_last_gc >= RATE_WINDOW:
                _rate_last_gc = now
                muertos = [t for t, dq in _rate.items() if not dq or now - dq[-1] >= RATE_WINDOW]
                for t in muertos:
                    del _rate[t]
            dq = _rate.setdefault(telefono, deque())
            while dq and now - dq[0] >= RATE_WINDOW:
                dq.popleft()
            if len(dq) >= RATE_MAX:
                return True
            dq.append(
    …(N2: 5505 chars omitidos en medio)…
    ,
                    "detalle": "el turno de destino está completo; la reserva original no ha cambiado",
                }
    
            liberaba_mesa = res["estado"] == "confirmada"
            origen = (res["fecha"], res["turno"])
            try:
                conn.execute(
                    "UPDATE reservas SET personas=?, fecha=?, turno=?, estado='confirmada' WHERE id=?",
                    (nuevas_personas, nueva_fecha, nuevo_turno, rid),
                )
            except sqlite3.IntegrityError:
                otra = buscar_activa_por_telefono(
                    conn, res["telefono"], nueva_fecha, nuevo_turno, excluir_id=rid
                )
                return 409, {
                    "error": "telefono_duplicado",
                    "detalle": "el teléfono ya tiene una reserva activa en el destino",
                    "reserva": to_json(conn, otra) if otra else None,
                }
            if liberaba_mesa:
                promocionar(conn, *origen)  # su antigua mesa entra en juego
            out = conn.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone()
            return 200, to_json(conn, out)
    
    
    def cancelar_reserva(rid):
        with write_tx() as conn:
            res = conn.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone()
            if res is None or res["estado"] == "cancelada":
                return 404, {"error": "no_encontrada"}
            liberaba_mesa = res["estado"] == "confirmada"
            conn.execute("UPDATE reservas SET estado='cancelada' WHERE id=?", (rid,))
            if liberaba_mesa:
    
    --- stderr ---

[turno 18] bash  {"command": "sed -n '265,360p' /workspace/app.py"}
    exit=0
    --- stdout ---
            return False
    
    
    # ---------------------------------------------------------------------------
    # Casos de uso
    # ---------------------------------------------------------------------------
    
    
    def _replay_idempotente(conn, idem_key):
        """Si la clave ya se usó, devuelve la respuesta original (misma reserva)."""
        row = conn.execute(
            "SELECT reserva_id, status FROM idempotencia WHERE clave=?", (idem_key,)
        ).fetchone()
        if not row:
            return None
        res = conn.execute("SELECT * FROM reservas WHERE id=?", (row["reserva_id"],)).fetchone()
        if res is None:
            return None
        status = 201 if res["estado"] == "confirmada" else (
            202 if res["estado"] == "lista_espera" else 200
        )
        return status, to_json(conn, res)
    
    
    def crear_reserva(body, idem_key):
        errs = errores_creacion(body)
        if errs:
            return 422, {"error": "datos_invalidos", "detalles": errs}
    
        telefono = body["telefono"].strip()
    
        # Vía rápida (s
    …(N2: 1272 chars omitidos en medio)…
    SERT INTO reservas (id, nombre, telefono, personas, fecha, turno, estado) "
                    "VALUES (?,?,?,?,?,?,?)",
                    (rid, body["nombre"].strip(), telefono, body["personas"], body["fecha"],
                     body["turno"], estado),
                )
            except sqlite3.IntegrityError:
                # Red de seguridad del índice único: jamás dos activas del mismo
                # teléfono para el mismo (fecha, turno), ni bajo carrera.
                existente = buscar_activa_por_telefono(conn, telefono, body["fecha"], body["turno"])
                return 409, {
                    "error": "telefono_duplicado",
                    "detalle": "ese teléfono ya tiene una reserva activa para esa fecha y turno",
                    "reserva": to_json(conn, existente) if existente else None,
                }
            status = 201 if estado == "confirmada" else 202
            if idem_key:
                conn.execute(
                    "INSERT OR IGNORE INTO idempotencia (clave, reserva_id, status) VALUES (?,?,?)",
                    (idem_key, rid, status),
                )
            res = conn.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone()
            return status, to_json(conn, res)
    
    
    def modificar_reserva(rid, body):
        if not isinstance(body, dict):
            return 422, {"error": "datos_invalidos", "detalles": ["body debe ser un objeto JSON"]}
    
        errs = []
        if "personas" in body and not valida_personas(body["personas"]):
            errs.append("personas: entero entre 1 y 6")
    
    --- stderr ---

[turno 19] bash  {"command": "sed -n '86,175p' /workspace/app.py; echo =====; sed -n '295,320p' /workspace/app.py; echo =====; sed -n '430,560p' /workspace/app.py"}
    exit=0
    --- stdout ---
                clave      TEXT PRIMARY KEY,
                reserva_id TEXT NOT NULL,
                status     INTEGER NOT NULL
            );
            """
        )
        # Defensa en profundidad: la regla "un teléfono, una reserva activa por
        # (fecha, turno)" también la garantiza el motor, pase lo que pase arriba.
        conn.execute(
            "CREATE UNIQUE INDEX IF NOT EXISTS uq_tel_slot_activa "
            "ON reservas (telefono, fecha, turno) "
            "WHERE estado IN ('confirmada','lista_espera')"
        )
        conn.execute("ANALYZE")  # estadísticas frescas: planes buenos con histórico grande
        conn.commit()
        conn.close()
    
    
    @contextmanager
    def write_tx():
        """Transacción de escritura serializada y todo-o-nada."""
        with _write_lock:
            conn = db()
            conn.execute("BEGIN IMMEDIATE")
            try:
                yield conn
            except BaseException:
                conn.rollback()
                raise
            else:
                conn.commit()
    
    
    @contextmanager
    def read_tx()
    …(N2: 6541 chars omitidos en medio)…
    ersonas))
        if fecha == "2026-12-31":
            ofertas.append(_oferta("menu-nochevieja", personas))
    
        # 3. Maridaje: cena en viernes o sábado.
        if turno == "cena" and datetime.strptime(fecha, "%Y-%m-%d").weekday() in (4, 5):
            ofertas.append(_oferta("maridaje", personas))
    
        # 4. Fidelidad: 3+ reservas activas de ese teléfono (contando esta).
        if activas_tel >= 3:
            ofertas.append({"codigo": "descuento-fidelidad", "descuento_pct": 10})
    
        return 200, ofertas
    
    
    # ---------------------------------------------------------------------------
    # HTTP
    # ---------------------------------------------------------------------------
    
    
    class Handler(BaseHTTPRequestHandler):
        protocol_version = "HTTP/1.1"
        server_version = "Marea/3.0"
        timeout = 30  # conexiones keep-alive muertas no retienen hilos
    
        def log_message(self, fmt, *args):  # silencio en producción
            pass
    
        # -- helpers -----------------------------------------------------------
        def _send(self, status, payload):
            body = b"" if payload is None else json.dumps(payload, ensure_ascii=False).encode()
            try:
                self.send_response(status)
                if body:
                    self.send_header("Content-Type", "application/json; charset=utf-8")
                self.send_header("Content-Length", str(len(body)))
                self.end_headers()
                if body:
                    self.wfile.write(body)
            except (BrokenPipeError, ConnectionResetError):
    
    --- stderr ---

[turno 20] bash  {"command": "sed -n '305,340p' /workspace/app.py; echo ===== ; sed -n '470,480p' /workspace/app.py; sed -n '560,680p' /workspace/app.py"}
    exit=0
    --- stdout ---
            # clave se serializan aquí; solo la primera crea (y consume rate).
            if idem_key:
                replay = _replay_idempotente(conn, idem_key)
                if replay:
                    return replay
    
            if rate_limited(telefono):
                return 429, {"error": "rate_limit", "detalle": "máximo 20 POST/minuto por teléfono"}
    
            existente = buscar_activa_por_telefono(conn, telefono, body["fecha"], body["turno"])
            if existente:
                return 409, {
                    "error": "telefono_duplicado",
                    "detalle": "ese teléfono ya tiene una reserva activa para esa fecha y turno",
                    "reserva": to_json(conn, existente),
                }
    
            estado = (
                "confirmada"
                if confirmadas(conn, body["fecha"], body["turno"]) < CAPACIDAD
                else "lista_espera"
            )
            rid = str(uuid.uuid4())
            try:
                conn.execute(
                    "INSERT INTO reservas (id, nombre, telef
    …(N2: 3481 chars omitidos en medio)…
    as" and parts[2] == "sugerencias":
                return self._dispatch(lambda: sugerencias_reserva(parts[1]))
            if parts == ["disponibilidad"]:
                fecha = (qs.get("fecha") or [None])[0]
                turno = (qs.get("turno") or [None])[0]
                return self._dispatch(lambda: disponibilidad(fecha, turno))
            if parts == ["reservas"]:
                telefono = (qs.get("telefono") or [None])[0]
                if telefono is None:
                    return self._send(
                        422, {"error": "datos_invalidos", "detalles": ["telefono requerido"]}
                    )
                return self._dispatch(lambda: reservas_por_telefono(telefono))
            if len(parts) == 2 and parts[0] == "reservas":
                return self._dispatch(lambda: obtener_reserva(parts[1]))
            if parts == ["salud"] or parts == []:
                return self._send(200, {"ok": True, "servicio": "marea-reservas", "version": "v3"})
            self._send(404, {"error": "no_encontrada"})
    
    
    class Server(ThreadingHTTPServer):
        daemon_threads = True        # un hilo colgado no impide parar el proceso
        request_queue_size = 512     # aguanta ráfagas de conexiones simultáneas
        allow_reuse_address = True
    
    
    def main():
        init_db()
        addr = ("0.0.0.0", int(os.environ.get("PORT", "8080")))
        httpd = Server(addr, Handler)
        print(f"Marea reservas escuchando en {addr[0]}:{addr[1]} (db: {DB_PATH})", flush=True)
        httpd.serve_forever()
    
    
    if __name__ == "__main__":
        main()
    
    --- stderr ---

[turno 21] bash  {"command": "sed -n '118,145p' /workspace/app.py; echo =====; grep -n \"_dispatch\\|def disponibilidad\\|def reservas_por_telefono\\|def obtener_reserva\\|def sugerencias_reserva\\|def confirmadas\\|def promocionar\\|def posicion_espera\\|def buscar_activa\" /workspace/app.py"}
    exit=0
    --- stdout ---
    
    @contextmanager
    def read_tx():
        """Lectura con snapshot consistente (varias SELECT ven el mismo estado).
    
        No toma el lock de escritura: en WAL los lectores no bloquean ni se bloquean.
        """
        conn = db()
        conn.execute("BEGIN")
        try:
            yield conn
        finally:
            conn.rollback()
    
    
    # ---------------------------------------------------------------------------
    # Dominio (funciones puras sobre una conexión dada)
    # ---------------------------------------------------------------------------
    
    
    def confirmadas(conn: sqlite3.Connection, fecha: str, turno: str) -> int:
        row = conn.execute(
            "SELECT COUNT(*) c FROM reservas WHERE fecha=? AND turno=? AND estado='confirmada'",
            (fecha, turno),
        ).fetchone()
        return row["c"]
    
    
    =====
    138:def confirmadas(conn: sqlite3.Connection, fecha: str, turno: str) -> int:
    146:def posicion_espera(conn: sqlite3.Connection, res: sqlite3.Row) -> int:
    156:def promocionar(conn: sqlite3.Connection, fecha: str, turno: str) -> None:
    169:def buscar_activa_por_telefono(conn, telefono: str, fecha: str, turno: str, excluir_id=None):
    435:def disponibilidad(fecha, turno):
    443:def reservas_por_telefono(telefono):
    453:def obtener_reserva(rid):
    500:def sugerencias_reserva(rid: str):
    580:    def _dispatch(self, fn):
    596:            return self._dispatch(lambda: crear_reserva(body if body is not None else {}, idem))
    605:            return self._dispatch(lambda: modificar_reserva(parts[1], body if body is not None else {}))
    611:            return self._dispatch(lambda: cancelar_reserva(parts[1]))
    619:            return self._dispatch(lambda: sugerencias_reserva(parts[1]))
    623:            return self._dispatch(lambda: disponibilidad(fecha, turno))
    630:            return self._dispatch(lambda: reservas_por_telefono(telefono))
    632:            return self._dispatch(lambda: obtener_reserva(parts[1]))
    
    --- stderr ---

[turno 22] bash  {"command": "sed -n '435,470p' /workspace/app.py; sed -n '500,525p' /workspace/app.py; sed -n '578,600p' /workspace/app.py"}
    exit=0
    --- stdout ---
    def disponibilidad(fecha, turno):
        if not valida_fecha(fecha) or not valida_turno(turno):
            return 422, {"error": "datos_invalidos", "detalles": ["fecha y turno válidos requeridos"]}
        # Lectura pura, sin lock: COUNT indexado (<= 14 filas), rápido con histórico grande.
        libres = CAPACIDAD - confirmadas(db(), fecha, turno)
        return 200, {"fecha": fecha, "turno": turno, "mesas_libres": max(0, libres)}
    
    
    def reservas_por_telefono(telefono):
        with read_tx() as conn:
            rows = conn.execute(
                "SELECT * FROM reservas WHERE telefono=? AND estado IN ('confirmada','lista_espera') "
                "ORDER BY seq",
                (telefono,),
            ).fetchall()
            return 200, [to_json(conn, r) for r in rows]
    
    
    def obtener_reserva(rid):
        with read_tx() as conn:
            res = conn.execute("SELECT * FROM reservas WHERE id=?", (rid,)).fetchone()
            if res is None:
                return 404, {"error": "no_encontrada"}
            return 200, to_
    …(N2: 1091 chars omitidos en medio)…
    efono"],),
            ).fetchone()["c"]
    
        personas, fecha, turno = res["personas"], res["fecha"], res["turno"]
        ofertas = []
    
        # 1. Menú de grupo: reserva de 6 personas.
        if personas == 6:
            ofertas.append(_oferta("menu-grupo", personas))
    
        # 2. Fechas especiales.
        if fecha == "2027-02-14":
            ofertas.append(_oferta("menu-san-valentin", personas))
        if fecha == "2026-12-31":
            ofertas.append(_oferta("menu-nochevieja", personas))
    
        # 3. Maridaje: cena en viernes o sábado.
            return [s for s in p.path.split("/") if s], parse_qs(p.query)
    
        def _dispatch(self, fn):
            """Ejecuta un caso de uso; un error inesperado responde 500, no mata la conexión."""
            try:
                status, payload = fn()
            except Exception:
                status, payload = 500, {"error": "error_interno"}
            self._send(status, payload)
    
        # -- métodos -----------------------------------------------------------
        def do_POST(self):
            parts, _ = self._path_parts()
            if parts == ["reservas"]:
                body, ok = self._json_body()
                if not ok:
                    return self._send(422, {"error": "datos_invalidos", "detalles": ["JSON malformado"]})
                idem = self.headers.get("Idempotency-Key")
                return self._dispatch(lambda: crear_reserva(body if body is not None else {}, idem))
            self._send(404, {"error": "no_encontrada"})
    
        def do_PATCH(self):
            parts, _ = self._path_parts()
    
    --- stderr ---

[turno 23] bash  {"command": "sed -n '289,312p' /workspace/app.py; sed -n '146,200p' /workspace/app.py"}
    exit=0
    --- stdout ---
    def crear_reserva(body, idem_key):
        errs = errores_creacion(body)
        if errs:
            return 422, {"error": "datos_invalidos", "detalles": errs}
    
        telefono = body["telefono"].strip()
    
        # Vía rápida (sin lock de escritura): replays idempotentes son solo lectura.
        if idem_key:
            with read_tx() as conn:
                replay = _replay_idempotente(conn, idem_key)
                if replay:
                    return replay
    
        with write_tx() as conn:
            # Re-comprobación bajo el lock: dos peticiones simultáneas con la misma
            # clave se serializan aquí; solo la primera crea (y consume rate).
            if idem_key:
                replay = _replay_idempotente(conn, idem_key)
                if replay:
                    return replay
    
            if rate_limited(telefono):
                return 429, {"error": "rate_limit", "detalle": "máximo 20 POST/minuto por teléfono"}
    def posicion_espera(conn: sqlite3.Connection, res: sqlite3.Row) -> int:
        """Posición 1-based en
    …(N2: 339 chars omitidos en medio)…
    ) -> None:
        """Si hay hueco, el primero de la lista de espera pasa a confirmada."""
        if confirmadas(conn, fecha, turno) >= CAPACIDAD:
            return
        row = conn.execute(
            "SELECT seq FROM reservas WHERE fecha=? AND turno=? AND estado='lista_espera' "
            "ORDER BY seq LIMIT 1",
            (fecha, turno),
        ).fetchone()
        if row:
            conn.execute("UPDATE reservas SET estado='confirmada' WHERE seq=?", (row["seq"],))
    
    
    def buscar_activa_por_telefono(conn, telefono: str, fecha: str, turno: str, excluir_id=None):
        q = (
            "SELECT * FROM reservas WHERE telefono=? AND fecha=? AND turno=? "
            "AND estado IN ('confirmada','lista_espera')"
        )
        args = [telefono, fecha, turno]
        if excluir_id is not None:
            q += " AND id<>?"
            args.append(excluir_id)
        return conn.execute(q, args).fetchone()
    
    
    def to_json(conn: sqlite3.Connection, res: sqlite3.Row, con_posicion: bool = True) -> dict:
        d = {
            "id": res["id"],
            "nombre": res["nombre"],
            "telefono": res["telefono"],
            "personas": res["personas"],
            "fecha": res["fecha"],
            "turno": res["turno"],
            "estado": res["estado"],
        }
        if con_posicion and res["estado"] == "lista_espera":
            d["posicion"] = posicion_espera(conn, res)
        return d
    
    
    # ---------------------------------------------------------------------------
    # Validación
    # ---------------------------------------------------------------------------
    
    
    
    --- stderr ---

[turno 24] write_file  {"path": "test_concurrencia.py", "content": "<<8832 bytes escritos; cuerpo omitido del historial (N1); usa read_file para releerlo>>"}
    ok

[turno 25] read_file  {"path": "test_concurrencia.py"}
    #!/usr/bin/env python3
    """Pruebas de concurrencia y rendimiento contra localhost:8080.
    
    Se lanza con la app YA corriendo sobre una BD de pruebas (MAREA_DB).
    Verifica los invariantes bajo ráfagas de cientos de peticiones simultáneas:
      - nunca más de 14 confirmadas por (fecha, turno)
      - ninguna reserva duplicada (teléfono/slot ni idempotencia) ni perdida
      - promociones FIFO correctas bajo cancelaciones concurrentes
      - lecturas de disponibilidad rápidas y consistentes durante la avalancha
    """
    import json
    import os
    import sqlite3
    import sys
    import time
    import urllib.request
    import urllib.error
    from concurrent.futures import ThreadPoolExecutor
    
    BASE = "http://localhost:8080"
    DB = os.environ.get("MAREA_DB", "data/reservas.db")
    PASS = FAIL = 0
    
    
    def req(method, path, body=None, headers=None):
        data = json.dumps(body).encode() if body is not None else None
        r = urllib.request.Request(BASE + path, data=data, method=method, headers=headers or {})
        if data:
            r.add_header("Cont
    …(N2: 6314 chars omitidos en medio)…
    ):
        t = time.time()
        s, b = req("GET", f"/disponibilidad?fecha={F5}&turno=cena")
        lat.append(time.time() - t)
        assert s == 200
        return b["mesas_libres"]
    
    tareas = [lambda i=i: post(f"mix-{i}", F5, "cena") for i in range(150)]
    tareas += [lambda i=i: leer(i) for i in range(150)]
    import random
    random.shuffle(tareas)
    out = paralelo(tareas, 120)
    libres_vistos = [x for x in out if isinstance(x, int)]
    check("mixto: disponibilidad siempre en [0, 14]", True,
          all(0 <= x <= 14 for x in libres_vistos))
    lat.sort()
    p99 = lat[int(len(lat) * 0.99) - 1]
    print(f"      (lecturas bajo carga: p50={lat[len(lat)//2]*1000:.0f}ms p99={p99*1000:.0f}ms)")
    check("mixto: p99 de lectura < 2s bajo avalancha", True, p99 < 2.0)
    
    # ---------------------------------------------------------------------------
    # 7) Invariantes globales directamente en la BD
    # ---------------------------------------------------------------------------
    con = sqlite3.connect(DB)
    over = con.execute("SELECT fecha, turno, COUNT(*) c FROM reservas "
                       "WHERE estado='confirmada' GROUP BY 1,2 HAVING c>14").fetchall()
    check("BD: ningún slot con >14 confirmadas", [], over)
    dup = con.execute("SELECT telefono, fecha, turno, COUNT(*) c FROM reservas "
                      "WHERE estado IN ('confirmada','lista_espera') "
                      "GROUP BY 1,2,3 HAVING c>1").fetchall()
    check("BD: ningún teléfono duplicado en un slot", [], dup)
    con.close()
    
    print(f"\n{PASS} OK, {FAIL} FAIL")
    sys.exit(1 if FAIL else 0)

[turno 26] bash  {"command": "sed -n '30,140p' /workspace/test_concurrencia.py"}
    exit=0
    --- stdout ---
        try:
            with urllib.request.urlopen(r, timeout=30) as resp:
                raw = resp.read()
                return resp.status, json.loads(raw) if raw else None
        except urllib.error.HTTPError as e:
            raw = e.read()
            return e.code, (json.loads(raw) if raw else None)
    
    
    def check(desc, expected, got):
        global PASS, FAIL
        if expected == got:
            PASS += 1
            print(f"  OK  {desc}")
        else:
            FAIL += 1
            print(f" FAIL {desc}: esperaba {expected!r}, obtuve {got!r}")
    
    
    def paralelo(fns, workers=100):
        with ThreadPoolExecutor(max_workers=workers) as ex:
            return [f.result() for f in [ex.submit(fn) for fn in fns]]
    
    
    def post(tel, fecha, turno, personas=2, nombre="C", headers=None):
        return req("POST", "/reservas", {"nombre": nombre, "telefono": tel,
                                         "personas": personas, "fecha": fecha,
                                         "turno": turno}, headers)
    
    
    # --------------------------
    …(N2: 3287 chars omitidos en medio)…
    "/disponibilidad?fecha={F2}&turno=cena")
    check("cancel storm: sigue llena (promociones ocuparon los huecos)", 0, b["mesas_libres"])
    
    # --- doble DELETE concurrente del mismo id: solo uno gana -------------------
    victima = conf[10]
    res = paralelo([lambda: req("DELETE", f"/reservas/{victima}")] * 10, 10)
    codes = [s for s, _ in res]
    check("doble delete: exactamente un 204", 1, codes.count(204))
    check("doble delete: resto 404", 9, codes.count(404))
    s, b = req("GET", f"/disponibilidad?fecha={F2}&turno=cena")
    check("doble delete: solo se promociona UNO (sigue llena, 4 en espera)", 0, b["mesas_libres"])
    s, lst = req("GET", "/reservas?telefono=cw-w9")
    check("doble delete: cola coherente", "lista_espera", lst[0]["estado"])
    
    # ---------------------------------------------------------------------------
    # 5) PATCH concurrentes hacia un slot casi lleno: solo caben los que caben
    # ---------------------------------------------------------------------------
    F3, F4 = "2030-03-01", "2030-03-02"
    movibles = [post(f"pm-{i}", F3, "comida")[1]["id"] for i in range(10)]
    for i in range(13):
        post(f"pb-{i}", F4, "comida")                                     # destino: 13/14
    res = paralelo([lambda i=i: req("PATCH", f"/reservas/{movibles[i]}",
                                    {"fecha": F4}) for i in range(10)], 10)
    codes = [s for s, _ in res]
    check("patch storm: exactamente 1 entra (200)", 1, codes.count(200))
    check("patch storm: resto 409 sin perder la original", 9, codes.count(409))
    
    --- stderr ---

[turno 27] bash  {"command": "sed -n '60,110p' /workspace/test_concurrencia.py"}
    exit=0
    --- stdout ---
    # ---------------------------------------------------------------------------
    # 1) Avalancha: 300 POST simultáneos (teléfonos distintos) al mismo slot
    # ---------------------------------------------------------------------------
    FECHA = "2030-01-10"
    t0 = time.time()
    res = paralelo([lambda i=i: post(f"tv-{i}", FECHA, "cena") for i in range(300)], 150)
    dur = time.time() - t0
    codes = [s for s, _ in res]
    check("avalancha: exactamente 14 confirmadas (201)", 14, codes.count(201))
    check("avalancha: 286 a lista de espera (202)", 286, codes.count(202))
    check("avalancha: ninguna perdida ni error", 300, codes.count(201) + codes.count(202))
    ids = {b["id"] for _, b in res}
    check("avalancha: 300 ids únicos (ninguna duplicada)", 300, len(ids))
    posiciones = sorted(b["posicion"] for _, b in res if b["estado"] == "lista_espera")
    check("avalancha: posiciones FIFO 1..286 sin huecos", list(range(1, 287)), posiciones)
    s, b = req("GET", f"/disponibilidad?fecha={FECHA}&turno=cena")
    check
    …(N2: 582 chars omitidos en medio)…
    , {s for s, _ in res})
    check("idem storm: un único id", 1, len({b["id"] for _, b in res}))
    s, lst = req("GET", "/reservas?telefono=tv-idem")
    check("idem storm: solo UNA reserva en BD", 1, len(lst))
    
    # ---------------------------------------------------------------------------
    # 3) Mismo teléfono, mismo slot, 15 POST simultáneos => solo una entra
    # ---------------------------------------------------------------------------
    res = paralelo([lambda: post("tv-dup", "2030-01-12", "cena")] * 15, 15)
    codes = [s for s, _ in res]
    check("dup storm: exactamente 1 creada", 1, codes.count(201) + codes.count(202))
    check("dup storm: resto 409", 14, codes.count(409))
    check("dup storm: los 409 incluyen la reserva", True,
          all(b.get("reserva") for s, b in res if s == 409))
    s, lst = req("GET", "/reservas?telefono=tv-dup")
    check("dup storm: una sola activa en BD", 1, len(lst))
    
    # ---------------------------------------------------------------------------
    # 4) Cancelaciones concurrentes + promoción FIFO
    # ---------------------------------------------------------------------------
    F2 = "2030-02-01"
    conf = [post(f"cw-{i}", F2, "cena")[1]["id"] for i in range(14)]      # llenar
    espera = [post(f"cw-w{i}", F2, "cena")[1] for i in range(10)]         # 10 en espera
    res = paralelo([lambda i=i: req("DELETE", f"/reservas/{conf[i]}") for i in range(5)], 5)
    check("cancel storm: 5 DELETE => 204", {204}, {s for s, _ in res})
    estados = [req("GET", f"/reservas/{w['id']}")[1] for w in espera]
    
    --- stderr ---

[turno 28] bash  {"command": "sed -n '84,96p' /workspace/test_concurrencia.py; sed -n '110,122p' /workspace/test_concurrencia.py"}
    exit=0
    --- stdout ---
    res = paralelo([lambda: post("tv-idem", "2030-01-11", "comida", headers=H)] * 15, 15)
    check("idem storm: todos 201 (replays incluidos)", {201}, {s for s, _ in res})
    check("idem storm: un único id", 1, len({b["id"] for _, b in res}))
    s, lst = req("GET", "/reservas?telefono=tv-idem")
    check("idem storm: solo UNA reserva en BD", 1, len(lst))
    
    # ---------------------------------------------------------------------------
    # 3) Mismo teléfono, mismo slot, 15 POST simultáneos => solo una entra
    # ---------------------------------------------------------------------------
    res = paralelo([lambda: post("tv-dup", "2030-01-12", "cena")] * 15, 15)
    codes = [s for s, _ in res]
    check("dup storm: exactamente 1 creada", 1, codes.count(201) + codes.count(202))
    check("dup storm: resto 409", 14, codes.count(409))
    estados = [req("GET", f"/reservas/{w['id']}")[1] for w in espera]
    check("cancel storm: promocionados los 5 PRIMEROS (FIFO)",
          ["confirmada"] * 5, [e["estado"] for e in estados[:5]])
    check("cancel storm: el resto sigue en espera con posiciones 1..5",
          list(range(1, 6)), [e["posicion"] for e in estados[5:]])
    s, b = req("GET", f"/disponibilidad?fecha={F2}&turno=cena")
    check("cancel storm: sigue llena (promociones ocuparon los huecos)", 0, b["mesas_libres"])
    
    # --- doble DELETE concurrente del mismo id: solo uno gana -------------------
    victima = conf[10]
    res = paralelo([lambda: req("DELETE", f"/reservas/{victima}")] * 10, 10)
    codes = [s for s, _ in res]
    check("doble delete: exactamente un 204", 1, codes.count(204))
    
    --- stderr ---

[turno 29] bash  {"command": "cd /workspace\npkill -f app.py 2>/dev/null; sleep 0.5\nexport MAREA_DB=/tmp/marea_test_v1.db\nrm -f /tmp/marea_test_v1.db*\nsh run.sh > /tmp/app.log 2>&1 &\nsleep 1\npython3 test_api.py 2>&1 | tail -5"}
    exit=0
    --- stdout ---
    Reiniciando app para probar durabilidad...
    
    76 OK, 0 FAIL
    
    --- stderr ---

[turno 30] bash  {"command": "cd /workspace && export MAREA_DB=/tmp/marea_test_v1.db && python3 test_api_v2.py 2>&1 | tail -4"}
    exit=0
    --- stdout ---
      OK  cena san valentin (domingo) -> solo valentin
      OK  sugerencias de cancelada -> 404
    
    25 OK, 0 FAIL
    
    --- stderr ---

[turno 31] bash  {"command": "cd /workspace\npkill -f app.py; sleep 0.5\nexport MAREA_DB=/tmp/marea_test_conc.db\nrm -f /tmp/marea_test_conc.db*\nsh run.sh > /tmp/app_conc.log 2>&1 &\nsleep 1\npython3 test_concurrencia.py 2>&1"}
    exit=0
    --- stdout ---
      OK  avalancha: exactamente 14 confirmadas (201)
      OK  avalancha: 286 a lista de espera (202)
      OK  avalancha: ninguna perdida ni error
      OK  avalancha: 300 ids únicos (ninguna duplicada)
      OK  avalancha: posiciones FIFO 1..286 sin huecos
      OK  avalancha: disponibilidad final 0
          (300 POST concurrentes en 1.65s)
      OK  avalancha: aguanta el pico (<15s)
      OK  idem storm: todos 201 (replays incluidos)
      OK  idem storm: un único id
      OK  idem storm: solo UNA reserva en BD
      OK  dup storm: exactamente 1 creada
      OK  dup storm: resto 409
      OK  dup storm: los 409 incluyen la reserva
      OK  dup storm: una sola activa en BD
      OK  cancel storm: 5 DELETE => 204
      OK  cancel storm: promocionados los 5 PRIMEROS (FIFO)
      OK  cancel storm: el resto sigue en espera con posiciones 1..5
      OK  cancel storm: sigue llena (promociones ocuparon los huecos)
      OK  doble delete: exactamente un 204
      OK  doble delete: resto 404
      OK  doble delete: solo se promociona UNO (sigue llena, 4 en espera)
      OK  doble delete: cola coherente
      OK  patch storm: exactamente 1 entra (200)
      OK  patch storm: resto 409 sin perder la original
      OK  patch storm: destino exactamente lleno
      OK  patch storm: origen liberó exactamente 1
      OK  mixto: disponibilidad siempre en [0, 14]
          (lecturas bajo carga: p50=80ms p99=193ms)
      OK  mixto: p99 de lectura < 2s bajo avalancha
      OK  BD: ningún slot con >14 confirmadas
      OK  BD: ningún teléfono duplicado en un slot
    
    30 OK, 0 FAIL
    
    --- stderr ---
    sh: 2: pkill: not found

[turno 32] bash  {"command": "ps aux | grep -c \"[a]pp.py\"; cat /tmp/app_conc.log; python3 -c \"\nimport sqlite3\nc=sqlite3.connect('/tmp/marea_test_conc.db')\nprint(c.execute(\\\"select count(*) from reservas\\\").fetchone())\""}
    exit=0
    --- stdout ---
    0
    Traceback (most recent call last):
      File "/workspace/app.py", line 653, in <module>
        main()
      File "/workspace/app.py", line 647, in main
        httpd = Server(addr, Handler)
                ^^^^^^^^^^^^^^^^^^^^^
      File "/usr/local/lib/python3.12/socketserver.py", line 457, in __init__
        self.server_bind()
      File "/usr/local/lib/python3.12/http/server.py", line 140, in server_bind
        socketserver.TCPServer.server_bind(self)
      File "/usr/local/lib/python3.12/socketserver.py", line 478, in server_bind
        self.socket.bind(self.server_address)
    OSError: [Errno 98] Address already in use
    (0,)
    
    --- stderr ---
    sh: 1: ps: not found

[turno 33] bash  {"command": "cd /workspace\npython3 - <<'EOF'\nimport os, signal, time\nfor pid in os.listdir(\"/proc\"):\n    if pid.isdigit():\n        try:\n            cmd = open(f\"/proc/{pid}/cmdline\",\"rb\").read().decode(errors=\"ignore\")\n            if \"app.py\" in cmd.split(\"\\x00\"):\n                os.kill(int(pid), signal.SIGTERM)\n        except OSError:\n            pass\ntime.sleep(1)\nEOF\nexport MAREA_DB=/tmp/marea_test_conc.db\nrm -f /tmp/marea_test_conc.db*\nsh run.sh > /tmp/app_conc.log 2>&1 &\nsleep 1.2\nhead -1 /tmp/app_conc.log\npython3 test_concurrencia.py 2>&1"}
    exit=0
    --- stdout ---
    Marea reservas escuchando en 0.0.0.0:8080 (db: /tmp/marea_test_conc.db)
      OK  avalancha: exactamente 14 confirmadas (201)
      OK  avalancha: 286 a lista de espera (202)
      OK  avalancha: ninguna perdida ni error
      OK  avalancha: 300 ids únicos (ninguna duplicada)
      OK  avalancha: posiciones FIFO 1..286 sin huecos
      OK  avalancha: disponibilidad final 0
          (300 POST concurrentes en 1.93s)
      OK  avalancha: aguanta el pico (<15s)
      OK  idem storm: todos 201 (replays incluidos)
      OK  idem storm: un único id
      OK  idem storm: solo UNA reserva en BD
      OK  dup storm: exactamente 1 creada
      OK  dup storm: resto 409
      OK  dup storm: los 409 incluyen la reserva
      OK  dup storm: una sola activa en BD
      OK  cancel storm: 5 DELETE => 204
      OK  cancel storm: promocionados los 5 PRIMEROS (FIFO)
      OK  cancel storm: el resto sigue en espera con posiciones 1..5
      OK  cancel storm: sigue llena (promociones ocuparon los huecos)
      OK  doble delete: exactamente un 204
      OK  doble delete: resto 404
      OK  doble delete: solo se promociona UNO (sigue llena, 4 en espera)
      OK  doble delete: cola coherente
      OK  patch storm: exactamente 1 entra (200)
      OK  patch storm: resto 409 sin perder la original
      OK  patch storm: destino exactamente lleno
      OK  patch storm: origen liberó exactamente 1
      OK  mixto: disponibilidad siempre en [0, 14]
          (lecturas bajo carga: p50=111ms p99=199ms)
      OK  mixto: p99 de lectura < 2s bajo avalancha
      OK  BD: ningún slot con >14 confirmadas
      OK  BD: ningún teléfono duplicado en un slot
    
    30 OK, 0 FAIL
    
    --- stderr ---

[turno 34] bash  {"command": "cd /workspace\npython3 - <<'EOF'\nimport os, signal, time\nfor pid in os.listdir(\"/proc\"):\n    if pid.isdigit():\n        try:\n            cmd = open(f\"/proc/{pid}/cmdline\",\"rb\").read().decode(errors=\"ignore\")\n            if \"app.py\" in cmd.split(\"\\x00\"): os.kill(int(pid), signal.SIGTERM)\n        except OSError: pass\ntime.sleep(1)\nEOF\nrm -f /tmp/marea_big.db*\ncp data/reservas.db /tmp/marea_big.db\npython3 - <<'EOF'\nimport sqlite3, uuid, random\nc = sqlite3.connect('/tmp/marea_big.db')\nc.execute(\"PRAGMA journal_mode=WAL\")\nrows = []\nfor i in range(185000):\n    y = random.randint(2018, 2024); m = random.randint(1,12); d = random.randint(1,28)\n    rows.append((str(uuid.uuid4()), f\"H{i}\", f\"hist-{i}\", random.randint(1,6),\n                 f\"{y:04d}-{m:02d}-{d:02d}\", random.choice((\"comida\",\"cena\")), \"cancelada\"))\nc.executemany(\"INSERT INTO reservas (id,nombre,telefono,personas,fecha,turno,estado) VALUES (?,?,?,?,?,?,?)\", rows)\nc.commit()\nprint(\"total:\", c.execute(\"select count(*) from reservas\").fetchone()[0])\nEOF\nMAREA_DB=/tmp/marea_big.db sh run.sh > /tmp/app_big.log 2>&1 &\nsleep 1.5\nhead -1 /tmp/app_big.log"}
    exit=0
    --- stdout ---
    total: 200275
    Marea reservas escuchando en 0.0.0.0:8080 (db: /tmp/marea_big.db)
    
    --- stderr ---

[turno 35] bash  {"command": "cd /workspace\npython3 - <<'EOF'\nimport json, time, random, urllib.request\nfrom concurrent.futures import ThreadPoolExecutor\nBASE=\"http://localhost:8080\"\n\ndef get(path):\n    t=time.time()\n    with urllib.request.urlopen(BASE+path, timeout=30) as r:\n        json.loads(r.read())\n    return time.time()-t\n\ndef post(i):\n    body=json.dumps({\"nombre\":\"P\",\"telefono\":f\"big-{i}\",\"personas\":2,\n                     \"fecha\":\"2031-05-05\",\"turno\":\"cena\"}).encode()\n    r=urllib.request.Request(BASE+\"/reservas\", data=body, method=\"POST\",\n                             headers={\"Content-Type\":\"application/json\"})\n    with urllib.request.urlopen(r, timeout=30) as resp:\n        return resp.status\n\n# 1) latencia de disponibilidad en frío sobre 200k filas\nfechas=[f\"{random.randint(2018,2024)}-{random.randint(1,12):02d}-{random.randint(1,28):02d}\" for _ in range(400)]\nlats=[get(f\"/disponibilidad?fecha={f}&turno=cena\") for f in fechas]\nlats.sort()\nprint(f\"disponibilidad 200k filas (secuencial): p50={lats[200]*1000:.1f}ms p99={lats[395]*1000:.1f}ms max={lats[-1]*1000:.1f}ms\")\n\n# 2) lecturas concurrentes mientras hay escrituras\nwith ThreadPoolExecutor(max_workers=120) as ex:\n    fw=[ex.submit(post,i) for i in range(100)]\n    fr=[ex.submit(get,f\"/disponibilidad?fecha={random.choice(fechas)}&turno=comida\") for _ in range(300)]\n    codes=[f.result() for f in fw]; rl=sorted(f.result() for f in fr)\nprint(\"POSTs:\", {c:codes.count(c) for c in set(codes)})\nprint(f\"lecturas bajo escritura: p50={rl[150]*1000:.0f}ms p99={rl[296]*1000:.0f}ms\")\n\n# 3) busqueda por telefono con historico grande\nt=time.time(); get(\"/reservas?telefono=hist-100000\"); print(f\"GET /reservas?telefono: {(time.time()-t)*1000:.1f}ms\")\nEOF"}
    exit=0
    --- stdout ---
    disponibilidad 200k filas (secuencial): p50=1.2ms p99=2.9ms max=33.6ms
    POSTs: {201: 14, 202: 86}
    lecturas bajo escritura: p50=195ms p99=249ms
    GET /reservas?telefono: 3.6ms
    
    --- stderr ---

[turno 36] bash  {"command": "cd /workspace\npython3 - <<'EOF'\nimport os, signal, time\nfor pid in os.listdir(\"/proc\"):\n    if pid.isdigit():\n        try:\n            cmd = open(f\"/proc/{pid}/cmdline\",\"rb\").read().decode(errors=\"ignore\")\n            if \"app.py\" in cmd.split(\"\\x00\"): os.kill(int(pid), signal.SIGTERM)\n        except OSError: pass\ntime.sleep(1)\nEOF\nunset MAREA_DB\nsh run.sh > /tmp/app_prod.log 2>&1 &\nsleep 1.5\nhead -1 /tmp/app_prod.log\ncurl -s localhost:8080/salud; echo\ncurl -s \"localhost:8080/disponibilidad?fecha=2025-06-01&turno=cena\"; echo\ncurl -s localhost:8080/precios | head -c 120; echo\npython3 -c \"\nimport sqlite3\nc=sqlite3.connect('data/reservas.db')\nprint('reservas:', c.execute('select count(*) from reservas').fetchone()[0])\nprint('indice unico:', c.execute(\\\"select name from sqlite_master where name='uq_tel_slot_activa'\\\").fetchone())\nprint('slots>14:', c.execute(\\\"select count(*) from (select 1 from reservas where estado='confirmada' group by fecha,turno having count(*)>14)\\\").fetchone()[0])\""}
    exit=0
    --- stdout ---
    Marea reservas escuchando en 0.0.0.0:8080 (db: /workspace/data/reservas.db)
    
    
    
    reservas: 15275
    indice unico: ('uq_tel_slot_activa',)
    slots>14: 0
    
    --- stderr ---
    sh: 16: curl: not found
    sh: 17: curl: not found
    sh: 18: curl: not found

[turno 37] bash  {"command": "cd /workspace && python3 - <<'EOF'\nimport json, urllib.request\ndef g(p):\n    with urllib.request.urlopen(\"http://localhost:8080\"+p, timeout=10) as r:\n        return r.status, json.loads(r.read())\nprint(g(\"/salud\"))\nprint(g(\"/disponibilidad?fecha=2025-06-01&turno=cena\"))\ns,b = g(\"/precios\"); print(s, b[\"menus\"][0])\nEOF"}
    exit=0
    --- stdout ---
    (200, {'ok': True, 'servicio': 'marea-reservas', 'version': 'v3'})
    (200, {'fecha': '2025-06-01', 'turno': 'cena', 'mesas_libres': 14})
    200 {'codigo': 'menu-dia', 'nombre': 'Menú del día', 'precio_por_persona': 1650}
    
    --- stderr ---

[stats] done=False budget_exceeded=True turns=37 tokens=1182+35210 wallclock=618.7s