{"record":{"id":"d0798654e7d7e082","repo":"moeru-ai/airi","slug":"chat-sync-pullmessages-failed-for","errorCode":null,"errorMessage":"[chat-sync] pullMessages failed for","messagePattern":"\\[chat-sync\\] pullMessages failed for","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"packages/stage-ui/src/stores/chat/session-store.ts","lineNumber":767,"sourceCode":"    if (!wsClient || wsClient.status() !== 'open')\n      return\n    const meta = sessionMetas.value[sessionId]\n    if (!meta?.cloudChatId)\n      return\n\n    try {\n      const result = await wsClient.pullMessages({\n        chatId: meta.cloudChatId,\n        afterSeq: meta.cloudMaxSeq ?? 0,\n      })\n      mergeCloudMessagesIntoSession(sessionId, {\n        messages: result.messages,\n        toSeq: result.seq,\n      })\n      cloudHydratedSessions.add(sessionId)\n    }\n    catch (err) {\n      console.warn('[chat-sync] pullMessages failed for', sessionId, errorMessageFrom(err))\n    }\n  }\n\n  /**\n   * Reconcile local sessions against the server `chats` table. Called after\n   * the local index loads and after every successful (re)connect.\n   *\n   * - Local sessions without a `cloudChatId` either claim a remote chat with\n   *   the same id or trigger `POST /api/v1/chats` to mint one.\n   * - Remote chats that have no local mapping are adopted as empty-shell\n   *   sessions; their messages are pulled lazily on first `loadSession`.\n   * - Remote chats whose id is in the user's tombstone set are skipped — the\n   *   user already deleted them locally and the server-side soft-delete may\n   *   not have committed yet.\n   *\n   * Reentrant: a single in-flight task is shared across concurrent callers.\n   * If a new \"open\" event fires while a reconcile is running, a follow-up\n   * pass is scheduled in `finally` so catch-up pulls do not get lost.","sourceCodeStart":749,"sourceCodeEnd":785,"githubUrl":"https://github.com/moeru-ai/airi/blob/b6d0809ecb6421d492c4ce38aa7a76146c83269d/packages/stage-ui/src/stores/chat/session-store.ts#L749-L785","documentation":"Warns when wsClient.pullMessages({ chatId, afterSeq }) rejects while lazily hydrating an adopted remote chat on first loadSession. mergeCloudMessagesIntoSession is skipped and the session is NOT added to cloudHydratedSessions, so opening the session again retries the pull. The session still opens, but shows only its empty shell until a pull succeeds.","triggerScenarios":"loadSession racing a disconnect (pull issued while the WebSocket RPC is down); the chat was deleted on the server so chatId is unknown; auth token expired mid-session; server 5xx during the hydration pull.","commonSituations":"Laptop resumes from sleep and the user opens an adopted chat before reconnect finishes; the chat was deleted from another device moments earlier; flaky network on first open of a remote-only session.","solutions":["Confirm the sync client is connected before opening remote-only sessions (reconnect re-runs hydration)","Close and reopen the session — cloudHydratedSessions was not marked, so loadSession retries the pull","If the chat was deleted remotely, run a reconcile (listChats) so the dead local mapping is cleaned up instead of retrying a dead chatId","For persistent failures, inspect server logs for pullMessages errors on that chatId and fix the auth/network cause"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"if (!meta.cloudChatId) return // pull only applies to cloud-mapped sessions\nif (cloudHydratedSessions.has(sessionId)) return // already hydrated","typeGuard":null,"tryCatchPattern":"try {\n  const result = await wsClient.pullMessages({ chatId: meta.cloudChatId, afterSeq: meta.cloudMaxSeq ?? 0 })\n  mergeCloudMessagesIntoSession(sessionId, { messages: result.messages, toSeq: result.seq })\n  cloudHydratedSessions.add(sessionId)\n}\ncatch (err) {\n  // do NOT add to cloudHydratedSessions — the next loadSession retries the pull\n  console.warn('[chat-sync] pullMessages failed for', sessionId, errorMessageFrom(err))\n}","preventionTips":["Open remote sessions only after the sync client reports connected","Treat a failed pull as 'not hydrated', never as 'empty session', to avoid acting on incomplete server state","Log meta.cloudMaxSeq with the failure to correlate with server-side seq gaps"],"tags":["chat-sync","websocket","lazy-hydration","messages","network"],"backgroundTag":"websocket-rpc-failed","analyzedSha":"b6d0809ecb6421d492c4ce38aa7a76146c83269d","analyzedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-08-23T13:39:53.451Z"}