April 22, 2026

[Foro de Telegram: Mensaje enviado al tema incorrecto tras reintento 529] - Telegram Forum: Message Sent to Wrong Topic After Anthropic 529 Retry

Cuando la API de Anthropic devuelve 529 (sobrecargada) y OpenClaw reintenta la solicitud, el mensaje de respuesta se envía sin el message_thread_id correcto, lo que provoca que los mensajes desaparezcan de los temas del foro.

🔍 Síntomas

Síntoma principal

Después de un ciclo de reintento HTTP 529 de la API de Anthropic, el mensaje de respuesta de Telegram se envía correctamente (la API de Telegram devuelve ok y un message_id válido), pero el mensaje no aparece en el tema del foro esperado.

Evidencia en registros

2026-03-03T11:19:05.208Z [agent/embedded] embedded run agent end: runId=561c9fa1 isError=true error=The AI service is temporarily overloaded.
2026-03-03T11:19:05.685Z [agent/embedded] embedded run agent end: runId=81dab484 isError=true error=The AI service is temporarily overloaded.
2026-03-03T11:24:34.955Z [telegram] sendMessage ok chat=-1003885638534 message=13832

Síntomas de diagnóstico

  • Sin error thread not found — Telegram no rechazó el ID del hilo
  • Sin message_thread_id en los registros — La salida de depuración omite el parámetro thread, ocultando el diagnóstico
  • Brecha de 5 minutos entre el último error 529 y sendMessage (indica reintento con retroceso)
  • Entradas de sesión faltantes — No hay registros de sesión para topic:562 en el día del incidente
  • Reinicio de socket obsoleto ocurrió 7 minutos después de sendMessage pero después de que el mensaje ya se perdió

Comportamiento visible para el usuario

  • El mensaje original en el tema 562 no recibe respuesta
  • El ID del mensaje de respuesta existe en la base de datos de Telegram (confirmado por respuesta de API)
  • El mensaje es invisible tanto en el tema objetivo COMO en el Tema General
  • El mensaje parece estar "enviado" pero está efectivamente huérfano

🧠 Causa raíz

Fallo principal: Pérdida de contexto de hilo durante reintento

La causa raíz es una falta de propagación de contexto en la tubería de reintentos. Cuando Anthropic devuelve HTTP 529, ocurre la siguiente secuencia:

  1. Mensaje recibido — OpenClaw recibe una actualización de Telegram que contiene message.chat.id, message.message_thread_id: 562, y contexto de conversación
  2. Llamada API iniciada — OpenClaw llama a la API de mensajes de Anthropic con el contexto de conversación
  3. Error 529 recibido — Anthropic devuelve HTTP 529: The AI service is temporarily overloaded
  4. Reintento activado — El mecanismo de reintento de OpenClaw (con retroceso) reintenta la llamada API
  5. Corrupción del contexto — Durante el ciclo de reintento, el message_thread_id de la actualización original de Telegram no se propaga a la llamada sendMessage

Problema arquitectónico: Estado de sesión vs. Contexto en línea

OpenClaw utiliza una arquitectura basada en sesiones donde el contexto de conversación se almacena en un almacén de sesiones. El error crítico ocurre cuando:

// Simplified flow showing the failure point
async function handleUpdate(update) {
  const threadId = update.message.message_thread_id; // 562 - captured here
  
  // On first attempt, session is created/loaded
  const session = await sessionStore.get(update.chat.id);
  session.threadId = threadId;
  await sessionStore.set(update.chat.id, session);
  
  // ... API call made, 529 received ...
  
  // On retry, session state may be stale or overwritten
  const retrySession = await sessionStore.get(update.chat.id);
  // retrySession.threadId could be undefined, null, or wrong value
  
  // sendMessage called without correct thread_id
  await telegram.sendMessage({
    chat_id: update.chat.id,
    text: response,
    message_thread_id: retrySession.threadId // BUG: undefined!
  });
}

Factores que contribuyen

  • El retraso de reintento crea condición de carrera — El retroceso de 5 minutos entre el 529 y el reintento permite que el estado de sesión se borre, corrompa o sobrescriba
  • Sin thread_id en registros de sendMessage — La declaración de depuración omite message_thread_id, previniendo la detección temprana:
    // Current (broken) log format
    console.log(`sendMessage ok chat=${chatId} message=${messageId}`);
    

    // Missing: message_thread_id=${threadId || ‘undefined’}

  • TTL/expiración del almacén de sesiones — Si las sesiones expiran durante la ventana de reintento, el contexto del hilo se pierde
  • Manejo de mensajes concurrentes — Si otro mensaje llega en un tema diferente durante el reintento, el estado de sesión puede ser sobrescrito

Por qué no se genera un error

Telegram acepta el mensaje sin message_thread_id porque por defecto lo envía al "Tema Principal" (thread_id: 0). Sin embargo, el comportamiento del Tema Principal en grupos de foro varía según el cliente y la versión de Telegram — algunos clientes ocultan estos mensajes completamente si el contexto original era de un hilo diferente.

🛠️ Solución paso a paso

Paso 1: Garantizar que el ID de hilo se pase a sendMessage

Modifica el adaptador de Telegram para incluir siempre message_thread_id en la carga útil de sendMessage, usando el valor del mensaje entrante si no está disponible desde el estado de sesión:

// BEFORE (broken implementation)
async sendMessage(chatId, text, options = {}) {
  const payload = {
    chat_id: chatId,
    text: text,
    // message_thread_id not included - defaults to 0/undefined
    ...options
  };
  
  const result = await this.telegram.sendMessage(payload);
  console.log(`sendMessage ok chat=${chatId} message=${result.message_id}`);
  return result;
}

// AFTER (fixed implementation)
async sendMessage(chatId, text, options = {}) {
  const payload = {
    chat_id: chatId,
    text: text,
    parse_mode: 'Markdown',
    ...options
    // message_thread_id MUST be passed explicitly in options
    // No defaulting to undefined - caller is responsible
  };
  
  // Enhanced logging with thread_id
  console.log(`sendMessage ok chat=${chatId} thread=${payload.message_thread_id ?? 'main'} message=${result.message_id}`);
  return result;
}

Paso 2: Preservar el ID de hilo a través de ciclos de reintento

Asegura que el thread_id del mensaje entrante se propague hasta la llamada sendMessage, independientemente del estado de sesión:

// BEFORE (session-dependent)
async handleMessage(ctx, messageText) {
  const session = await this.getSession(ctx.chat.id);
  const response = await this.callAIWithRetry(messageText, session.context);
  
  // Thread ID from session - may be stale after retry
  await this.telegram.sendMessage(ctx.chat.id, response, {
    message_thread_id: session.threadId
  });
}

// AFTER (incoming message context preserved)
async handleMessage(ctx, messageText) {
  // Capture thread_id from the ACTUAL incoming message, not session
  const originalThreadId = ctx.message.message_thread_id;
  
  const session = await this.getSession(ctx.chat.id);
  const response = await this.callAIWithRetry(messageText, session.context);
  
  // Always use the original message's thread_id
  await this.telegram.sendMessage(ctx.chat.id, response, {
    message_thread_id: originalThreadId
  });
}

Paso 3: Agregar ID de hilo a todas las operaciones de envío

Asegura que todos los métodos de envío de Telegram incluyan thread_id cuando operen en un contexto de foro:

// Helper to build send options with thread context
function buildSendOptions(originalMessage, overrides = {}) {
  const options = { ...overrides };
  
  // Always include thread_id if original message had one
  if (originalMessage.message_thread_id) {
    options.message_thread_id = originalMessage.message_thread_id;
  }
  
  return options;
}

// Usage
const sendOptions = buildSendOptions(ctx.message);
await this.telegram.sendMessage(ctx.chat.id, text, sendOptions);
await this.telegram.editMessageReplyMarkup(ctx.chat.id, messageId, sendOptions);

Paso 4: Mejorar el registro de reintentos

Registra el thread_id en cada intento de reintento para ayudar a la depuración:

async callAIWithRetry(message, context, threadId) {
  const maxRetries = 3;
  let lastError;
  
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    console.log(`[retry] attempt=${attempt} thread=${threadId} maxRetries=${maxRetries}`);
    
    try {
      return await this.anthropic.messages.create({
        model: 'claude-3-5-sonnet-20241022',
        max_tokens: 1024,
        messages: [{ role: 'user', content: message }],
        extra_headers: { 'anthropic-dangerous-direct-browser-access': 'true' }
      });
    } catch (error) {
      lastError = error;
      
      if (error.status === 529) {
        console.log(`[retry] received 529 (overloaded) thread=${threadId}`);
        const backoffMs = Math.min(1000 * Math.pow(2, attempt), 30000);
        console.log(`[retry] backing off for ${backoffMs}ms thread=${threadId}`);
        await sleep(backoffMs);
      } else if (error.status === 529) {
        throw error; // Non-retryable error
      }
    }
  }
  
  throw lastError;
}

Paso 5: Bloqueo de estado de sesión (Avanzado)

Previene la corrupción del estado de sesión durante ciclos de reintento largos:

// Use optimistic locking for session updates
async updateSession(chatId, updater, threadId) {
  const maxAttempts = 3;
  
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const session = await this.sessionStore.get(chatId);
    const updated = updater(session);
    
    // Preserve thread_id across session updates
    updated.threadId = session.threadId || threadId;
    
    try {
      await this.sessionStore.set(chatId, updated);
      return updated;
    } catch (conflictError) {
      if (attempt === maxAttempts) throw conflictError;
      await sleep(50 * attempt); // Brief backoff
    }
  }
}

🧪 Verificación

Paso 1: Reproducir el escenario 529

Simula un error 529 de Anthropic para activar la ruta de reintento:

# Using curl to simulate the Telegram update webhook
curl -X POST http://localhost:3000/webhook/telegram \
  -H "Content-Type: application/json" \
  -d '{
    "update_id": 123456789,
    "message": {
      "message_id": 100,
      "chat": { "id": -1003885638534, "type": "supergroup" },
      "message_thread_id": 562,
      "text": "Test message for 529 retry scenario"
    }
  }'

Paso 2: Verificar la salida del registro de sendMessage

Después de aplicar la corrección, confirma que el registro incluye thread:

# Expected log output AFTER fix
2026-03-03T11:24:34.955Z [telegram] sendMessage ok chat=-1003885638534 thread=562 message=13832

# Should NOT see (before fix):
2026-03-03T11:24:34.955Z [telegram] sendMessage ok chat=-1003885638534 message=13832

Paso 3: Verificar que el mensaje aparece en el tema correcto

# Use Telegram's getMessage to verify thread placement
curl "https://api.telegram.org/bot${BOT_TOKEN}/getMessage?chat_id=-1003885638534&message_id=13832"

# Expected response includes:
{
  "ok": true,
  "result": {
    "message_id": 13832,
    "chat": { "id": -1003885638534, "type": "supergroup" },
    "message_thread_id": 562,  // <-- Must match original
    "text": "..."
  }
}

Paso 4: Verificar que la sesión contiene el ID de hilo

# Check session store for correct thread_id
# (depends on session store implementation)

# If using Redis:
redis-cli GET "session:-1003885638534"
# Should contain: {"threadId": 562, "..."}

# If using file-based:
cat sessions/-1003885638534.json
# Should contain: {"threadId": 562, "..."}

Paso 5: Prueba unitaria para preservación de contexto de hilo

describe('Telegram forum thread context', () => {
  it('should preserve message_thread_id through 529 retry', async () => {
    const ctx = createMockContext({
      chatId: -1003885638534,
      messageId: 100,
      threadId: 562,
      text: 'Test message'
    });
    
    // Mock Anthropic to return 529 twice, then success
    aiClient.messages.create
      .mockRejectedValueOnce({ status: 529, message: 'overloaded' })
      .mockRejectedValueOnce({ status: 529, message: 'overloaded' })
      .mockResolvedValueOnce({ content: [{ type: 'text', text: 'Response' }] });
    
    await handler.handleUpdate(ctx);
    
    // Verify sendMessage was called with correct thread_id
    expect(telegramAdapter.sendMessage).toHaveBeenCalledWith(
      -1003885638534,
      expect.any(String),
      expect.objectContaining({ message_thread_id: 562 })
    );
  });
});

Paso 6: Prueba de integración con entorno de prueba de Telegram

# Use Telegram's test environment or a private bot
# Send message in a forum topic, trigger 529 error, verify reply location

# 1. Set BOT_TOKEN to test bot
export BOT_TOKEN="test_bot_token"

# 2. Run openclaw with logging
OPENCLAW_LOG_LEVEL=debug npm start

# 3. Monitor for:
# - sendMessage logs with thread=562
# - Message appears in correct topic
# - No "lost" messages

⚠️ Errores comunes

Trampas específicas del entorno

  • El reinicio del contenedor Docker borra el estado de sesión

    Si OpenClaw se ejecuta en Docker y el contenedor se reinicia durante un ciclo de reintento largo, el estado de sesión (incluyendo thread_id) se pierde. Asegura que el almacén de sesiones esté externalizado (Redis) en lugar de en memoria.

    # Docker Compose configuration - externalize session storage
    services:
      openclaw:
        image: openclaw:latest
        environment:
          - SESSION_STORE=redis
          - REDIS_URL=redis://redis:6379
      redis:
        image: redis:7-alpine
        volumes:
          - redis-data:/data
    volumes:
      redis-data:
    
  • Límites de descriptores de archivo en macOS

    Cuando se usan sesiones basadas en archivos en macOS, el ulimit por defecto puede causar fallos de escritura de sesión durante alta carga:

    # Check current limit
    ulimit -n
    # Increase if below 1024
    ulimit -n 65535
    
  • Separadores de ruta en claves de sesión en Windows

    Las rutas de archivos del almacén de sesiones pueden tener problemas en Windows con caracteres especiales en los ID de chat (guión inicial):

    # Use encodeURIComponent for chat IDs in file paths
    const sessionPath = path.join(
      sessionDir,
      `${encodeURIComponent(String(chatId))}.json`
    );
    

Errores de configuración

  • Olvidar habilitar el soporte de foro en BotFather

    Los bots de Telegram requieren permiso de grupo_membership explícito para temas de foro:

    # Required BotFather commands:
    # /setprivacy -> Disable (for forum access)
    # /setjoingroup -> Yes
    # /setforums -> Enable (if available)
    
  • TTL de sesión desajustado vs. retroceso de reintento

    Si el TTL de sesión es más corto que el período de retroceso de reintento, el contexto del hilo expira:

    # Example: 5-minute TTL but 5-minute backoff = guaranteed context loss
    SESSION_TTL=300000  # 5 minutes in ms
    MAX_RETRY_BACKOFF=300000  # Should be less than TTL
    
  • Usar reply_to_message_id sin message_thread_id

    Incluso con reply_to_message_id configurado correctamente, omitir message_thread_id causa que los mensajes del foro se pierdan:

    # BROKEN: reply without thread context
    {
      chat_id: -1003885638534,
      text: "Reply text",
      reply_to_message_id: 100
      // Missing: message_thread_id: 562
    }
    

    CORRECT: include both

    { chat_id: -1003885638534, text: “Reply text”, reply_to_message_id: 100, message_thread_id: 562 }

Errores a nivel de código

  • Almacenar thread_id como cadena vs. número

    La API de Telegram acepta ambos pero mezclar tipos causa problemas:

    # Telegram API is flexible but some clients expect integer
    const threadId = parseInt(message.message_thread_id, 10);
    # Or ensure consistent type
    const threadId = String(message.message_thread_id);
    
  • Sobrescribir sesión en manejadores concurrentes

    Si múltiples mensajes llegan simultáneamente para el mismo chat, las escrituras de sesión pueden competir:

    // PROBLEMATIC: Read-modify-write without atomicity
    const session = await getSession(chatId);
    session.threadId = threadId;  // Read
    await saveSession(chatId, session);  // Write - another request may overwrite
    

    // FIXED: Use atomic operations or locking await updateSessionAtomic(chatId, (s) => { s.threadId = threadId; return s; });

  • Condiciones de carrera async/await en manejadores de reintento

    La cadena de callback/promesa puede perder contexto:

    // PROBLEMATIC
    function handleMessage(ctx) {
      let threadId = ctx.message.message_thread_id;
    

    retry(3, () => ai.call()).then(response => { // ’this’ and ’threadId’ may be out of scope or stale sendMessage(ctx.chat.id, response, { threadId }); });

    // New message arrives, ’threadId’ is overwritten threadId = newMessage.message_thread_id; }

    // FIXED: Capture context in closure function handleMessage(ctx) { const threadId = ctx.message.message_thread_id; // Capture immediately

    retry(3, () => ai.call()).then(response => { sendMessage(ctx.chat.id, response, { message_thread_id: threadId }); }); }

🔗 Errores relacionados

  • HTTP 529: The AI service is temporarily overloaded

    Error de limitación de tasa de Anthropic que activa la secuencia de reintento. El error 529 es el evento iniciador del bug.

  • stale-socket

    Reinicio del monitor de salud del gateway que ocurrió 7 minutos después del mensaje perdido. No está directamente relacionado pero indica inestabilidad de conexión subyacente que puede exacerbar los problemas de reintento.

  • thread not found (no visto en este caso)
    Error de API de Telegram cuando message_thread_id se refiere a un tema inexistente. La ausencia de este error confirma que Telegram recibió un ID de hilo válido (o ningún ID de hilo en absoluto).

  • Expiración de sesión durante operaciones de larga duración

    Relacionado con el issue de GitHub donde el TTL de sesión es más corto que la duración de la operación, causando pérdida de contexto. Causa raíz similar a la pérdida de thread_id.

  • Fallo de entrega de webhook con contexto de hilo faltante

    Issue relacionado donde las actualizaciones de webhook de Telegram llegan sin message_thread_id para mensajes de foro, causando fallos de enrutamiento.

  • context.lengthExceeded Error de Anthropic

    Cuando el contexto de conversación crece demasiado durante los reintentos, Anthropic devuelve este error. Puede agravar los problemas de contexto de hilo si el manejo de errores pierde estado.

  • Condición de carrera en actualizaciones concurrentes de Telegram

    Cuando múltiples actualizaciones llegan simultáneamente para el mismo chat, el estado de sesión puede ser sobrescrito, perdiendo thread_id. Misma vulnerabilidad arquitectónica que este bug.

  • Mensaje enviado al chat incorrecto después de refrescar el token del bot

    Escenario de pérdida de sesión/contexto relacionado donde los cambios de configuración del bot a mitad de operación causan que los mensajes se enruten incorrectamente.

Evidencia y fuentes

Esta guía de solución de problemas fue sintetizada automáticamente por la tubería de inteligencia de FixClaw a partir de las discusiones de la comunidad.