moeru-ai/airi · warning

[chat-sync] outbox drain failed for

Error message

[chat-sync] outbox drain failed for

What it means

The batched outbox drain failed to deliver all entries of one session in a single sendMessages call. Every entry in that failing session batch gets attempts + 1 and the shared lastError, while other sessions that succeeded are dequeued via dequeueOutbox(succeededIds). One poison message fails its whole session batch; the drain is single-flight guarded and safe to retrigger.

Source

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

      for (const [sessionId, sessionEntries] of bySession) {
        const meta = sessionMetas.value[sessionId]
        const cloudChatId = meta?.cloudChatId
        if (!cloudChatId)
          continue
        if (!wsClient || wsClient.status() !== 'open')
          break

        sessionEntries.sort((a, b) => a.queuedAt - b.queuedAt)
        try {
          await wsClient.sendMessages({
            chatId: cloudChatId,
            messages: sessionEntries.map(e => ({ id: e.messageId, role: e.role, content: e.content })),
          })
          succeededIds.push(...sessionEntries.map(e => e.messageId))
        }
        catch (err) {
          const errMsg = errorMessageFrom(err) ?? 'unknown'
          console.warn('[chat-sync] outbox drain failed for', sessionId, errMsg)
          for (const entry of sessionEntries) {
            failedUpdates.push({
              messageId: entry.messageId,
              attempts: entry.attempts + 1,
              lastError: errMsg,
            })
          }
        }
      }

      if (succeededIds.length > 0)
        await enqueuePersist(() => chatSessionsRepo.dequeueOutbox(userId, succeededIds))
      if (failedUpdates.length > 0)
        await enqueuePersist(() => chatSessionsRepo.updateOutboxEntries(userId, failedUpdates))
      await refreshOutboxPendingCount()
    })().finally(() => {
      outboxDrainTask = undefined
    })

View on GitHub (pinned to b6d0809ecb)

Solutions

  1. Restore connectivity and let the single-flight drain retry on its next trigger
  2. Find the poison message: entries with growing attempts/lastError in the outbox repo identify it
  3. If one message always fails its session batch, fix or remove it (e.g. re-mint the chatId) to unblock the rest
  4. When the whole batch keeps failing with not-found, verify the chat still exists server-side
Defensive patterns

Strategy: retry

Validate before calling

if (sessionEntries.length === 0) continue // nothing to send for this session
if (!cloudChatId) continue // no mapping yet

Try / catch

try {
  await wsClient.sendMessages({ chatId: cloudChatId, messages: sessionEntries.map(e => ({ id: e.messageId, role: e.role, content: e.content })) })
  succeededIds.push(...sessionEntries.map(e => e.messageId))
}
catch (err) {
  // fail only this session's batch; other sessions continue draining
  for (const entry of sessionEntries)
    failedUpdates.push({ messageId: entry.messageId, attempts: entry.attempts + 1, lastError: errorMessageFrom(err) ?? 'unknown' })
}

Prevention

When it happens

Trigger: Batch drain over a flaky connection; one malformed or oversized message in a session batch failing the entire sendMessages call; chat deleted remotely before the backlog drained.

Common situations: Startup drain of an offline backlog; resuming after a long sleep; a single invalid message repeatedly blocking its session's batch.

Related errors


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