fix(app): paginate canonical timeline catch-up to avoid relay 1009 disconnect loop (#657)

@fireblue diagnosed the production failure mode: long canonical timeline catch-up could exceed the relay WebSocket frame cap, close with 1009, and send clients into a reconnect loop. Their seatbelt fix also removed the `limit: 0` footgun that let app callers request an unbounded frame.

This maintainer pass keeps that diagnosis and redirects the UX around bounded projected pages:

- initial agent load fetches the latest canonical tail
- app resume/reconnect re-fetches the latest tail instead of chaining after-cursor pages
- in-stream gaps still use bounded after-cursor catch-up
- older history loads explicitly when the user scrolls to the oldest edge
- shared page size is TIMELINE_FETCH_PAGE_SIZE = 100 projected timeline items, matching the daemon's canonical projection semantics rather than raw delta chunks

Original diagnosis and fix by @fireblue.

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
fireblue
2026-05-02 19:53:50 +08:00
committed by GitHub
co-authored by Mohamed Boudra
parent 906205f97a
commit 654439005a
18 changed files with 728 additions and 185 deletions
@@ -56,6 +56,8 @@ import type {
import type { AgentScreenAgent } from "@/hooks/use-agent-screen-state-machine";
import { useSessionStore } from "@/stores/session-store";
import { useFileExplorerActions } from "@/hooks/use-file-explorer-actions";
import { useLoadOlderAgentHistory } from "@/hooks/use-load-older-agent-history";
import type { ToastApi } from "@/components/toast-host";
import type { DaemonClient } from "@server/client/daemon-client";
import { ToolCallDetailsContent } from "./tool-call-details";
import { QuestionFormCard } from "./question-form-card";
@@ -145,6 +147,7 @@ export interface AgentStreamViewProps {
pendingPermissions: Map<string, PendingPermission>;
routeBottomAnchorRequest?: BottomAnchorRouteRequest | null;
isAuthoritativeHistoryReady?: boolean;
toast?: ToastApi | null;
onOpenWorkspaceFile?: (input: { filePath: string }) => void;
}
@@ -158,6 +161,7 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
pendingPermissions,
routeBottomAnchorRequest = null,
isAuthoritativeHistoryReady = true,
toast,
onOpenWorkspaceFile,
},
ref,
@@ -198,6 +202,11 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
workspaceId: workspaceId ?? undefined,
workspaceRoot,
});
const { isLoadingOlder, hasOlder, loadOlder } = useLoadOlderAgentHistory({
serverId: resolvedServerId,
agentId,
toast,
});
const openWorkspaceFile = useStableEvent(function openWorkspaceFile(input: {
filePath: string;
}) {
@@ -796,6 +805,9 @@ const AgentStreamViewComponent = forwardRef<AgentStreamViewHandle, AgentStreamVi
routeBottomAnchorRequest,
isAuthoritativeHistoryReady,
onNearBottomChange: setIsNearBottom,
onNearHistoryStart: loadOlder,
isLoadingOlderHistory: isLoadingOlder,
hasOlderHistory: hasOlder,
scrollEnabled: streamScrollEnabled,
listStyle: stylesheet.list,
baseListContentContainerStyle: stylesheet.listContentContainer,