moeru-ai/airi · warning

[chat-sync] listChats failed; skipping reconcile this round:

Error message

[chat-sync] listChats failed; skipping reconcile this round:

What it means

Reconcile cannot start this round: mapper.listChats() rejected, so the whole local-vs-remote reconciliation is skipped via early return. Local sessions do not get cloud ids minted, remote chats are not adopted, and tombstones are not drained until the next round. Reconcile re-runs after every successful (re)connect, so divergence is temporary once connectivity recovers.

Source

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

    const myEpoch = reconcileEpoch
    const isStaleEpoch = () => myEpoch !== reconcileEpoch

    const reconcileTask = (async () => {
      const currentUserId = getCurrentUserId()
      if (currentUserId === 'local') {
        console.info('[chat-sync] reconcile skipped: anonymous user')
        return
      }

      console.info('[chat-sync] reconcile start', { userId: currentUserId, serverUrl: SERVER_URL })
      const mapper = getCloudMapper()

      let remoteChats
      try {
        remoteChats = await mapper.listChats()
      }
      catch (err) {
        console.warn('[chat-sync] listChats failed; skipping reconcile this round:', errorMessageFrom(err))
        return
      }
      if (isStaleEpoch())
        return
      console.info('[chat-sync] listChats →', remoteChats.length, 'remote chats')

      // Snapshot local metas owned by this user. Anonymous-era sessions are
      // not promoted to the cloud automatically — the user can re-open them
      // after signing in and the server is unaware of them.
      const localOwnedMetas = Object.values(sessionMetas.value).filter(meta => meta.userId === currentUserId)
      const plan = reconcileLocalAndRemote(localOwnedMetas, remoteChats)

      // Tombstones: drop adopt entries for chats the user already deleted.
      // The server's soft-delete may not have committed yet (offline DELETE
      // path), so we still need to remember "do not re-adopt this id".
      const tombstones = await chatSessionsRepo.getTombstones(currentUserId)
      if (isStaleEpoch())
        return

View on GitHub (pinned to b6d0809ecb)

Solutions

  1. Check that the hosted backend stack (server/docker-compose.yaml) is running and SERVER_URL is correct
  2. Re-authenticate if the underlying status is 401/403
  3. Do nothing for transient outages — reconcile re-runs after every successful reconnect
  4. Inspect the network request for GET /api/v1/chats to confirm status code and CORS/proxy behavior
  5. Rely on the outbox to keep local changes queueing so a skipped round never loses data
Defensive patterns

Strategy: retry

Validate before calling

if (!currentUserId) return // source already skips reconcile for anonymous users

Try / catch

let remoteChats
try {
  remoteChats = await mapper.listChats()
}
catch (err) {
  console.warn('[chat-sync] listChats failed; skipping reconcile this round:', errorMessageFrom(err))
  return // next successful (re)connect re-runs reconcile
}
if (isStaleEpoch()) return

Prevention

When it happens

Trigger: Reconcile round starting while the backend is unreachable; auth token expired (401 on GET /api/v1/chats); proxy/CORS misconfiguration blocking the list call; API app down or returning 5xx.

Common situations: Backend down for maintenance while the client keeps scheduling reconcile rounds; token refresh missing so 401 repeats; SERVER_URL pointing at an old deployment.

Understand the failure class

Background: 'Something went wrong' / 'Request failed (500)' / 'HTTP error! status: 404' — what failed HTTP requests actually mean and how to find the real cause — this error's family across 28 libraries.

Related errors


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