[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_iden 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:562en 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:
- Mensaje recibido — OpenClaw recibe una actualización de Telegram que contiene
message.chat.id,message.message_thread_id: 562, y contexto de conversación - Llamada API iniciada — OpenClaw llama a la API de mensajes de Anthropic con el contexto de conversación
- Error 529 recibido — Anthropic devuelve
HTTP 529: The AI service is temporarily overloaded - Reintento activado — El mecanismo de reintento de OpenClaw (con retroceso) reintenta la llamada API
- Corrupción del contexto — Durante el ciclo de reintento, el
message_thread_idde 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_idsinmessage_thread_idIncluso 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 overloadedError de limitación de tasa de Anthropic que activa la secuencia de reintento. El error 529 es el evento iniciador del bug.
stale-socketReinicio 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.lengthExceededError de AnthropicCuando 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.