-
Notifications
You must be signed in to change notification settings - Fork 88
fix: pagination timeout no longer clears isLoadingMoreMessages #9327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -401,7 +401,9 @@ public final class ChatViewModel: ObservableObject { | |
| /// True while a previous-page load is in progress (brief async delay for UX). | ||
| @Published public var isLoadingMoreMessages: Bool = false | ||
|
|
||
| /// Timeout task that resets `isLoadingMoreMessages` if the daemon never responds. | ||
| /// Timeout task that logs a warning if the daemon takes too long to respond | ||
| /// to a pagination request. The flag is intentionally NOT cleared here — | ||
| /// see the comment in `loadPreviousMessagePage()` for rationale. | ||
| private var loadMoreTimeoutTask: Task<Void, Never>? | ||
|
|
||
| /// The subset of messages that are actually displayed (excludes subagent notifications | ||
|
|
@@ -460,13 +462,20 @@ public final class ChatViewModel: ObservableObject { | |
| // All local messages are visible — fetch the next page from the daemon. | ||
| guard hasMoreHistory, let cursor = historyCursor, let sessionId else { return false } | ||
| isLoadingMoreMessages = true | ||
| // Safety timeout: if the daemon drops the connection or never responds, | ||
| // reset the flag so the user can retry. | ||
| // Safety timeout: log a warning if the daemon is slow, but do NOT | ||
| // clear isLoadingMoreMessages here. Callers (ThreadSessionRestorer, | ||
| // IOSThreadStore) use `vm.isLoadingMoreMessages` to decide whether | ||
| // a history response is a pagination load. If the timeout clears the | ||
| // flag before the response arrives, the late-but-valid response is | ||
| // misclassified as an initial load and replaces all messages instead | ||
| // of prepending. The flag is properly cleared by populateFromHistory | ||
| // when the response arrives, or by reconnect/thread-switch logic if | ||
| // the daemon disconnects. | ||
| loadMoreTimeoutTask?.cancel() | ||
| loadMoreTimeoutTask = Task { @MainActor [weak self] in | ||
| try? await Task.sleep(nanoseconds: 15_000_000_000) // 15 seconds | ||
| guard let self, self.isLoadingMoreMessages else { return } | ||
| self.isLoadingMoreMessages = false | ||
| try? await Task.sleep(nanoseconds: 30_000_000_000) // 30 seconds | ||
| guard let self, !Task.isCancelled, self.isLoadingMoreMessages else { return } | ||
| log.warning("Pagination request still pending after 30s — daemon may be unresponsive") | ||
| } | ||
|
Comment on lines
+476
to
479
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If the daemon never returns a Useful? React with 👍 / 👎. |
||
| onLoadMoreHistory?(sessionId, cursor) | ||
| // The loading indicator is cleared by populateFromHistory when the response arrives. | ||
|
|
@@ -478,6 +487,9 @@ public final class ChatViewModel: ObservableObject { | |
| displayedMessageCount = Self.messagePageSize | ||
| historyCursor = nil | ||
| hasMoreHistory = false | ||
| loadMoreTimeoutTask?.cancel() | ||
| loadMoreTimeoutTask = nil | ||
| isLoadingMoreMessages = false | ||
| } | ||
|
|
||
| // MARK: - On-Demand Content Rehydration | ||
|
|
@@ -2161,6 +2173,7 @@ public final class ChatViewModel: ObservableObject { | |
| messageLoopTask?.cancel() | ||
| streamingFlushTask?.cancel() | ||
| cancelTimeoutTask?.cancel() | ||
| loadMoreTimeoutTask?.cancel() | ||
| // refinementFailureDismissTask and refinementFlushTask are accessed via | ||
| // @MainActor computed properties (forwarded from ChatMessageManager), which | ||
| // cannot be referenced from nonisolated deinit. Both tasks use [weak self], | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 Removing timeout reset of
isLoadingMoreMessagescauses permanent spinner when daemon silently drops a pagination requestIf the daemon silently drops or never responds to a pagination
history_request(without fully disconnecting),isLoadingMoreMessageswill remaintrueforever. The guard atChatViewModel.swift:444(guard hasMoreMessages, !isLoadingMoreMessages else { return false }) then blocks all future pagination attempts, leaving the user stuck with a permanent loading spinner and no way to retry.Root Cause and Impact
The old code had a 15-second safety timeout that would reset
isLoadingMoreMessages = false, allowing the user to retry. This PR removes that reset and only logs a warning (ChatViewModel.swift:478). The comment at line 472 claims the flag is "properly cleared by … reconnect/thread-switch logic if the daemon disconnects," but this is not accurate:daemonDidReconnectobserver (ChatViewModel.swift:641-711) never touchesisLoadingMoreMessages.resetMessagePagination()is dead code: The method atChatViewModel.swift:486does clear the flag, but it is never called from anywhere in the codebase — no caller exists in shared, macOS, or iOS code.ThreadManager.trimPreviousThreadIfNeededatThreadManager.swift:648actually skips trimming whenisLoadingMoreMessagesis true, but doesn't reset it.The only live paths that clear
isLoadingMoreMessagesare: (a)populateFromHistorywhenisPaginationLoadis true (ChatViewModel.swift:2107), and (b) error paths inThreadSessionRestorer.requestPaginatedHistory/IOSThreadStore.requestPaginatedHistorywhen the send fails synchronously. None of these help when the request is sent successfully but the daemon silently drops it.Impact: The user sees an infinite spinner at the top of the message list and cannot load older messages. The only recovery is switching threads or restarting the app.
Prompt for agents
Was this helpful? React with 👍 or 👎 to provide feedback.