{"record":{"id":"5e7cd81320089286","repo":"moeru-ai/airi","slug":"chat-sync-sendmessages-failed-for","errorCode":null,"errorMessage":"[chat-sync] sendMessages failed for","messagePattern":"\\[chat-sync\\] sendMessages failed for","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/stage-ui/src/stores/chat/session-store.ts","lineNumber":1154,"sourceCode":"\n    // Opportunistic immediate send. Skip if WS not open or cloudChatId not\n    // yet bound — drainOutbox will pick it up on the next reconcile.\n    if (!wsClient || wsClient.status() !== 'open')\n      return\n    if (!entry.cloudChatId)\n      return\n\n    try {\n      await wsClient.sendMessages({\n        chatId: entry.cloudChatId,\n        messages: [{ id: entry.messageId, role: entry.role, content: entry.content }],\n      })\n      await enqueuePersist(() => chatSessionsRepo.dequeueOutbox(userId, [entry.messageId]))\n      await refreshOutboxPendingCount()\n    }\n    catch (err) {\n      const errMsg = errorMessageFrom(err) ?? 'unknown'\n      console.warn('[chat-sync] sendMessages failed for', sessionId, errMsg)\n      await enqueuePersist(() => chatSessionsRepo.updateOutboxEntries(userId, [{\n        messageId: entry.messageId,\n        attempts: 1,\n        lastError: errMsg,\n      }]))\n    }\n  }\n\n  /**\n   * Drain every outbox entry for the current user via batched\n   * `sendMessages` calls (one per session). Idempotent and safe to call\n   * concurrently — a single-flight guard collapses overlapping triggers.\n   *\n   * Drain ordering: entries are grouped by sessionId, sorted by `queuedAt`\n   * within each session, and sent in a single batch per session. Server\n   * accepts client-supplied message ids so retries are idempotent.\n   *\n   * Entries whose session has no `cloudChatId` yet are skipped (they will","sourceCodeStart":1136,"sourceCodeEnd":1172,"githubUrl":"https://github.com/moeru-ai/airi/blob/b6d0809ecb6421d492c4ce38aa7a76146c83269d/packages/stage-ui/src/stores/chat/session-store.ts#L1136-L1172","documentation":"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.","triggerScenarios":"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.","commonSituations":"Sending messages offline (they queue in the outbox); resuming online with a backlog; server restarted and lost in-memory chat state.","solutions":["Restore connectivity; the outbox drain retries queued entries automatically","Read entry.lastError from the outbox repo to classify the failure (auth vs unknown chat vs validation)","If the chat was deleted remotely, force a reconcile so the session re-mints a cloudChatId, then resend","Fix the attempts accounting: use entry.attempts + 1 (as the batch path does) so chronic failures become visible to eviction logic","Watch the outbox pending count (refreshOutboxPendingCount-driven UI) so stalled messages are noticed"],"exampleFix":"// before\nawait enqueuePersist(() => chatSessionsRepo.updateOutboxEntries(userId, [{\n  messageId: entry.messageId,\n  attempts: 1, // resets the counter on every single-send failure\n  lastError: errMsg,\n}]))\n\n// after: increment like the batch drain path does\nawait enqueuePersist(() => chatSessionsRepo.updateOutboxEntries(userId, [{\n  messageId: entry.messageId,\n  attempts: entry.attempts + 1,\n  lastError: errMsg,\n}]))","handlingStrategy":"retry","validationCode":"if (!entry.cloudChatId) return // reconcile must mint a cloudChatId before a send is possible","typeGuard":null,"tryCatchPattern":"catch (err) {\n  const errMsg = errorMessageFrom(err) ?? 'unknown'\n  console.warn('[chat-sync] sendMessages failed for', sessionId, errMsg)\n  await enqueuePersist(() => chatSessionsRepo.updateOutboxEntries(userId, [{\n    messageId: entry.messageId,\n    attempts: entry.attempts + 1, // increment, do not reset to 1\n    lastError: errMsg,\n  }]))\n}","preventionTips":["Increment attempts instead of resetting so chronic failures become visible to eviction logic","Drain the outbox after every reconnect, not only on new sends","Surface outbox pending counts in the UI so users notice stalled messages","Classify lastError (auth vs not-found vs validation) before retrying blindly"],"tags":["chat-sync","outbox","sendmessages","retry"],"backgroundTag":"outbox-delivery-failed","analyzedSha":"b6d0809ecb6421d492c4ce38aa7a76146c83269d","analyzedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-08-23T11:17:13.642Z"}