moeru-ai/airi · warning

[chat-sync] sendMessages failed for

Error message

[chat-sync] sendMessages failed for

What it means

A single queued outbox entry could not be delivered via wsClient.sendMessages; the entry stays queued and the failure is recorded in lastError. Note this path writes attempts: 1 literally instead of incrementing (the batch drain path uses entry.attempts + 1), so failure counts reset on every single-send retry. The outbox drain retries the message later.

Source

Thrown at packages/stage-ui/src/stores/chat/session-store.ts:1154

    // Opportunistic immediate send. Skip if WS not open or cloudChatId not
    // yet bound — drainOutbox will pick it up on the next reconcile.
    if (!wsClient || wsClient.status() !== 'open')
      return
    if (!entry.cloudChatId)
      return

    try {
      await wsClient.sendMessages({
        chatId: entry.cloudChatId,
        messages: [{ id: entry.messageId, role: entry.role, content: entry.content }],
      })
      await enqueuePersist(() => chatSessionsRepo.dequeueOutbox(userId, [entry.messageId]))
      await refreshOutboxPendingCount()
    }
    catch (err) {
      const errMsg = errorMessageFrom(err) ?? 'unknown'
      console.warn('[chat-sync] sendMessages failed for', sessionId, errMsg)
      await enqueuePersist(() => chatSessionsRepo.updateOutboxEntries(userId, [{
        messageId: entry.messageId,
        attempts: 1,
        lastError: errMsg,
      }]))
    }
  }

  /**
   * Drain every outbox entry for the current user via batched
   * `sendMessages` calls (one per session). Idempotent and safe to call
   * concurrently — a single-flight guard collapses overlapping triggers.
   *
   * Drain ordering: entries are grouped by sessionId, sorted by `queuedAt`
   * within each session, and sent in a single batch per session. Server
   * accepts client-supplied message ids so retries are idempotent.
   *
   * Entries whose session has no `cloudChatId` yet are skipped (they will

View on GitHub (pinned to b6d0809ecb)

Solutions

  1. Restore connectivity; the outbox drain retries queued entries automatically
  2. Read entry.lastError from the outbox repo to classify the failure (auth vs unknown chat vs validation)
  3. If the chat was deleted remotely, force a reconcile so the session re-mints a cloudChatId, then resend
  4. Fix the attempts accounting: use entry.attempts + 1 (as the batch path does) so chronic failures become visible to eviction logic
  5. Watch the outbox pending count (refreshOutboxPendingCount-driven UI) so stalled messages are noticed

Example fix

// before
await enqueuePersist(() => chatSessionsRepo.updateOutboxEntries(userId, [{
  messageId: entry.messageId,
  attempts: 1, // resets the counter on every single-send failure
  lastError: errMsg,
}]))

// after: increment like the batch drain path does
await enqueuePersist(() => chatSessionsRepo.updateOutboxEntries(userId, [{
  messageId: entry.messageId,
  attempts: entry.attempts + 1,
  lastError: errMsg,
}]))
Defensive patterns

Strategy: retry

Validate before calling

if (!entry.cloudChatId) return // reconcile must mint a cloudChatId before a send is possible

Try / catch

catch (err) {
  const errMsg = errorMessageFrom(err) ?? 'unknown'
  console.warn('[chat-sync] sendMessages failed for', sessionId, errMsg)
  await enqueuePersist(() => chatSessionsRepo.updateOutboxEntries(userId, [{
    messageId: entry.messageId,
    attempts: entry.attempts + 1, // increment, do not reset to 1
    lastError: errMsg,
  }]))
}

Prevention

When it happens

Trigger: Flushing an outbox entry while the connection dropped; server rejecting the message (unknown chatId after the chat was deleted remotely, payload validation failure); auth expiry mid-drain.

Common situations: Sending messages offline (they queue in the outbox); resuming online with a backlog; server restarted and lost in-memory chat state.

Related errors


AI-assisted analysis of moeru-ai/airi@b6d0809ecb (2026-08-18). Data as JSON: /api/errors/5e7cd81320089286. Report an issue: GitHub.