Skip to content

Chat: PollTimeoutError Fallback Fetch Skipped — Stale chatId State on New Chats

Metadata

  • Date: 2026-05-25
  • Status: fixed
  • Severity: high
  • Related issue/ticket: feature/gpu-retry-notification
  • Owner: N/A

About

Overview: - After sending a chat message, users on a new chat (no existing chatId) saw "Sorry, I couldn't process that. Please try again." even though the backend saved the answer successfully. Closing and reopening the chat revealed the answer was there. - The bug caused unnecessary user-visible errors and eroded trust in the chat feature on every slow GPU inference (>5 min poll timeout).

Technical Questions: - Was the prior fix (commit 3634e23c) actually running? No — the guard condition if (placeholderId && chatId) silently short-circuited because chatId state was stale undefined for new chats. - Why is chatId stale? React's setState is asynchronous. setChatId(chat_id) schedules an update, but the chatId variable in the askQuestion closure still holds the value from the render when askQuestion was called (which is undefined for new chats). - Why can't the catch block use chat_id from the response? The local chat_id variable is declared inside the try block and is out of scope in catch.

Resources: - main/app/app/chat.tsxaskQuestion() function - main/app/lib/api/chats/pollForMessage.tsPollTimeoutError definition - main/app/__tests__/chat-screen-polling.test.tsx — reproduction test added

Steps to cause failure

flowchart LR
  A["User opens fresh chat screen\n(no chatId in params)"] --> B["Sends a message"]
  B --> C["chatWithMemory resolves\nchat_id='abc', message_id='xyz'"]
  C --> D["setChatId('abc') scheduled\n(async — state still undefined)"]
  C --> E["pollForMessage starts\nwaits up to 5min"]
  E --> F["PollTimeoutError thrown\nafter maxWaitMs"]
  F --> G{"if (placeholderId && chatId)\nchatId is still undefined"}
  G -->|"false — skipped"| H["Error message shown\n'Sorry, I couldn't process that'"]
  G -.->|"expected true"| I["Fallback getChatMessages fetch\nwould show the ready answer"]

System

flowchart TD
  ChatScreen["chat.tsx askQuestion()"] -->|"POST /memories/chat"| Dispatcher
  Dispatcher -->|"chat_id + message_id + status=thinking"| ChatScreen
  ChatScreen -->|"setChatId(chat_id) [async]"| ReactState["React state chatId\n(async — stale in closure)"]
  ChatScreen -->|"pollForMessage({chat_id, message_id})"| Poller
  Poller -->|"PollTimeoutError after 5min"| Catch["catch PollTimeoutError"]
  Catch -->|"reads chatId (stale: undefined)"| Guard{"placeholderId && chatId"}
  Guard -->|"false for new chats"| ErrorMsg["Error bubble shown"]
  Guard -.->|"should be true"| FallbackFetch["getChatMessages fallback"]

Reproduction Details

  1. Open the chat screen with no pre-existing chatId (new conversation).
  2. Send a message that triggers GPU inference taking longer than maxWaitMs (5 minutes).
  3. PollTimeoutError is thrown after the timeout.
  4. catch block evaluates if (placeholderId && chatId)chatId is undefined because setChatId() hasn't flushed yet.
  5. The fallback getChatMessages fetch is skipped; user sees the error message.

Reproduction test: main/app/__tests__/chat-screen-polling.test.tsx — "shows answer from fallback fetch when PollTimeoutError fires on a new chat (stale chatId state)"

Notes for PR

The root cause is the React async state update anti-pattern: storing a value via setState and then reading it in the same async closure. The fix introduces a mutable let resolvedChatId variable declared before the try block, initialized from the current chatId state (for existing chats) and updated synchronously alongside setChatId(chat_id) (for new chats). The PollTimeoutError catch block now reads resolvedChatId instead of chatId, ensuring the correct value is always available regardless of React's batching.

Audit Log

ID Action Note Context
1 Create audit log Initialize bug investigation Bug described in task
2 Read chat.tsx Confirmed chatId (state) used in catch block Lines 173-194
3 Read pollForMessage.ts Confirmed PollTimeoutError is thrown after maxWaitMs Line 74
4 Identified root cause setChatId() async; chatId undefined in catch for new chats React async state anti-pattern
5 Added failing test "shows answer from fallback fetch when PollTimeoutError fires on a new chat" Confirmed test fails before fix
6 Applied fix let resolvedChatId var captures chat_id synchronously alongside setChatId() chat.tsx askQuestion()
7 Confirmed test passes All 5 chat-screen-polling tests pass Full suite: 477 pass

Verification

  • [x] Reproduced failure before fix
  • [x] Reproduction test fails before fix
  • [x] Root cause identified with evidence
  • [x] Fix applied at source (no workaround-only patch)
  • [x] Reproduction test passes after fix
  • [x] Reproduction path now passes
  • [x] Regression test added/updated
  • [x] Verified no duplicate solved-bug log exists for same root cause