nanocoai/nanoclaw · warning

Message marked as failed after max retries

Error message

Message marked as failed after max retries

What it means

A message that kept getting stuck and retried has exhausted MAX_TRIES, so the sweep marks it failed in inbound.db instead of re-queueing again. After this warn, the message will not be delivered — the agent never processes it unless manually requeued.

Solutions

  1. Inspect the message content for something that crashes the agent (huge attachment, weird format) — a poison message
  2. Fix the underlying per-attempt failure (container crash, provider error) seen in logs before the retries exhausted
  3. Manually reset the message's failed status / tries in inbound.db to retry once fixed
  4. If failures are environmental flakiness, raise MAX_TRIES or backoff tuning

Example fix

-- retry a failed message once root cause is fixed (session inbound.db)
UPDATE messages_in SET status='pending', tries=0 WHERE id='<messageId>';
Defensive patterns

Strategy: validation

Validate before calling

// inbound.db check before it hits MAX_TRIES
const tries = await inDb.getMessageTries(msgId);
if (tries >= MAX_TRIES - 1) {
  // pull the message aside for manual inspection instead of letting it fail silently
}

Prevention

When it happens

Trigger: resetStuckProcessingRows sees msg.tries >= MAX_TRIES for a stuck/claimed message whose process_after has elapsed; markMessageFailed(msg.id) is called with the reset reason (e.g. 'claim-stuck', 'absolute-ceiling').

Common situations: Repeated container crashes on the same message (poison message); provider auth failure causing every retry to fail; persistent container instability making each attempt die mid-processing.

Related errors


AI-assisted analysis of nanocoai/nanoclaw@294ef2aee8 (2026-08-28). Data as JSON: /api/errors/e2fdc11ac7f52c12. Report an issue: GitHub.

Appendix: source

Thrown at src/host-sweep.ts:324

  inDb: InboundMailbox,
  outDb: OutboundMailbox,
  session: Session,
  reason: string,
): void {
  const claims = outDb.getProcessingClaims();
  const now = Date.now();
  for (const { messageId } of claims) {
    const msg = inDb.getMessageForRetry(messageId, 'pending');
    if (!msg) continue;

    // Already rescheduled for a future retry — don't bump tries again. The
    // wake path (sweep step 2) will fire when process_after elapses and a
    // fresh container will clean the orphan claim on startup.
    if (msg.processAfter && Date.parse(msg.processAfter) > now) continue;

    if (msg.tries >= MAX_TRIES) {
      inDb.markMessageFailed(msg.id);
      log.warn('Message marked as failed after max retries', {
        messageId: msg.id,
        sessionId: session.id,
        reason,
      });
    } else {
      const backoffMs = BACKOFF_BASE_MS * Math.pow(2, msg.tries);
      const backoffSec = Math.floor(backoffMs / 1000);
      inDb.retryWithBackoff(msg.id, backoffSec);
      log.info('Reset stale message with backoff', {
        messageId: msg.id,
        tries: msg.tries,
        backoffMs,
        reason,
      });
    }
  }

  // Drop the orphan 'processing' rows. Without this, the next sweep tick

View on GitHub (pinned to 294ef2aee8)