🔥 → 🔍 → ✅

Ayer por la noche, XoxoBot (mi chatbot axolotl 🪼) dejó de funcionar. El mensaje: "Ups, algo falló 😅 Intenta de nuevo."

Last night, XoxoBot (my axolotl chatbot 🪼) stopped working. The message: "Oops, something went wrong 😅 Try again."

No era el tipo de error que te dice qué pasó. Era el peor tipo: un catch genérico que esconde el problema real. Pero tengo un arma secreta: OscarCode9, mi agente de IA corriendo en OpenClaw. Le dije que lo arreglara, y esto es lo que encontró.

It wasn't the kind of error that tells you what happened. It was the worst kind: a generic catch that hides the real problem. But I have a secret weapon: OscarCode9, my AI agent running in OpenClaw. I told it to fix it, and this is what it found.

El ProblemaThe Problem

XoxoBot usa la API de GitHub Copilot para generar respuestas. El flujo de autenticación era así:

XoxoBot uses the GitHub Copilot API to generate responses. The authentication flow was like this:

1
Widget genera un clientId único por usuarioWidget generates a unique clientId per user
2
API busca auth del usuario en base de datosAPI looks up user auth in the database
3
Si no existe → Pide device flow OAuth con GitHubIf not found → Requests device flow OAuth with GitHub
4
Usuario autoriza con scope read:userUser authorizes with scope read:user
5
API intenta intercambiar token por Copilot API token → 💥 FAILAPI attempts to exchange token for Copilot API token → 💥 FAIL

🎯 Problema #1: El scope read:user no tiene permisos para acceder a la API de Copilot. Necesitas una suscripción activa de GitHub Copilot.

🎯 Problem #1: The read:user scope doesn't have permissions to access the Copilot API. You need an active GitHub Copilot subscription.

🎯 Problema #2: El token OAuth (ghu_...) del servidor había expirado. Estos tokens duran ~8 horas.

🎯 Problem #2: The server's OAuth token (ghu_...) had expired. These tokens last ~8 hours.

El DiagnósticoThe Diagnosis

OscarCode9 hizo lo que haría cualquier dev decente: revisar los logs.

OscarCode9 did what any decent dev would do: check the logs.

docker logs ovents-landing
Chat API error: Error: Copilot token exchange failed: HTTP 403
    at getCopilotApiToken (.next/server/app/api/chat/route.js:170:1031)

Un 403 Forbidden. El token existía pero GitHub lo rechazaba. Confirmado: token expirado.

A 403 Forbidden. The token existed but GitHub rejected it. Confirmed: expired token.

La SoluciónThe Solution

En lugar de arreglar el device flow (que requeriría que cada usuario tenga Copilot), implementamos un fallback inteligente:

Instead of fixing the device flow (which would require every user to have Copilot), we implemented an intelligent fallback:

1. Fallback al Token del Servidor1. Fallback to the Server Token

Si el usuario no tiene auth, en lugar de pedir device flow, usamos el token del servidor:

If the user has no auth, instead of requesting device flow, we use the server token:

lib/copilot-api.ts
export async function getCopilotApiTokenForClient(clientId?: string) {
  // Try user's own token if they have one
  if (clientId) {
    const auth = await prisma.copilotAuth.findUnique({ where: { clientId } });
    if (auth) {
      try {
        // ... intentar usar token del usuario
      } catch (e) {
        console.warn(`User token failed, using server token`);
      }
    }
  }
  
  // Fallback to server's Copilot token (always available)
  return getCopilotApiToken();
}

2. Soporte para Tokens Directos2. Support for Direct Tokens

Descubrimos que OpenClaw ya tenía un token válido guardado. El problema: era un token Copilot API (tid=...), no un token OAuth (ghu_...).

We discovered that OpenClaw already had a valid token stored. The problem: it was a Copilot API token (tid=...), not an OAuth token (ghu_...).

Agregamos detección automática:

We added automatic detection:

lib/copilot-api.ts
// If token is already a Copilot API token, use it directly
if (githubToken.startsWith("tid=")) {
  // Extract expiration from token: exp=<timestamp>
  const expMatch = githubToken.match(/exp=(\d+)/);
  const expiresAt = expMatch ? parseInt(expMatch[1], 10) * 1000 : Date.now() + 30 * 60 * 1000;
  
  cachedCopilotToken = { token: githubToken, expiresAt };
  return {
    token: githubToken,
    baseUrl: deriveCopilotBaseUrl(githubToken),
  };
}

✅ Resultado: El bot ahora acepta tanto tokens OAuth (ghu_...) como tokens Copilot directos (tid=...). Si uno falla, usa el otro.

✅ Result: The bot now accepts both OAuth tokens (ghu_...) and direct Copilot tokens (tid=...). If one fails, it uses the other.

El DeployThe Deploy

El fix requirió:

The fix required:

  1. Modificar lib/copilot-api.ts
  2. Modify lib/copilot-api.ts
  3. Actualizar el token en producción
  4. Update the token in production
  5. Rebuild del container Docker
  6. Rebuild the Docker container
# Subir archivo modificado
scp -i ~/TenK/truck.pem lib/copilot-api.ts ec2-user@3.145.149.229:~/ovents-landing/lib/

# Rebuild sin cache (importante!)
ssh ... "docker build --no-cache -t ovents-landing:prod ."

# Recrear container con el mapping correcto
docker run -d --name ovents-landing -p 3001:3001 \
  --env-file ~/ovents-landing/.env \
  --network backend_tenk-network \
  ovents-landing:prod

⚠️ Nota importante: El port mapping debe ser -p 3001:3001, NO -p 3001:3000. Este error causa 502 Bad Gateway y me ha mordido múltiples veces.

⚠️ Important note: The port mapping must be -p 3001:3001, NOT -p 3001:3000. This error causes 502 Bad Gateway and has bitten me multiple times.

Lecciones AprendidasLessons Learned

  1. Los tokens OAuth expiran. Los ghu_ de GitHub duran ~8 horas. Implementa refresh o fallbacks.
  2. OAuth tokens expire. GitHub's ghu_ tokens last ~8 hours. Implement refresh or fallbacks.
  3. El scope importa. read:user no da acceso a Copilot. Cada API tiene sus permisos.
  4. Scope matters. read:user doesn't give access to Copilot. Each API has its permissions.
  5. Fallbacks > Errores. Es mejor que el bot use un token del servidor que mostrar "algo falló".
  6. Fallbacks > Errors. It's better for the bot to use a server token than to show "something went wrong".
  7. Docker cache te puede trollear. Si cambias un archivo, usa --no-cache o Docker no lo detecta.
  8. Docker cache can troll you. If you change a file, use --no-cache or Docker won't detect it.
  9. Los agentes de IA son útiles para debugging. OscarCode9 encontró el problema, propuso la solución, y deployó. Yo solo aprobé.
  10. AI agents are useful for debugging. OscarCode9 found the problem, proposed the solution, and deployed. I just approved.

Tiempo TotalTotal Time

📊
~15 minutosminutes de diagnóstico + fix + deployof diagnosis + fix + deploy
Hecho por OscarCode9 (agente de IA) mientras yo veía los logsDone by OscarCode9 (AI agent) while I watched the logs
XoxoBot

XoxoBot 🪼

El asistente que casi muere por un token expirado, pero sobrevivió gracias a un fallback elegante. Ahora más fuerte que nunca.

The assistant that almost died from an expired token, but survived thanks to an elegant fallback. Now stronger than ever.