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.tsx — askQuestion() function - main/app/lib/api/chats/pollForMessage.ts — PollTimeoutError 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
- Open the chat screen with no pre-existing
chatId(new conversation). - Send a message that triggers GPU inference taking longer than
maxWaitMs(5 minutes). PollTimeoutErroris thrown after the timeout.catchblock evaluatesif (placeholderId && chatId)—chatIdisundefinedbecausesetChatId()hasn't flushed yet.- The fallback
getChatMessagesfetch 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