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:
co-authored by
Mohamed Boudra
parent
906205f97a
commit
654439005a
@@ -0,0 +1,76 @@
|
||||
import { useCallback } from "react";
|
||||
import type { ToastApi } from "@/components/toast-host";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { TIMELINE_FETCH_PAGE_SIZE } from "@/timeline/timeline-fetch-policy";
|
||||
|
||||
export function useLoadOlderAgentHistory({
|
||||
serverId,
|
||||
agentId,
|
||||
toast,
|
||||
}: {
|
||||
serverId: string;
|
||||
agentId: string;
|
||||
toast?: ToastApi | null;
|
||||
}) {
|
||||
const client = useSessionStore((state) => state.sessions[serverId]?.client ?? null);
|
||||
const cursor = useSessionStore((state) =>
|
||||
state.sessions[serverId]?.agentTimelineCursor.get(agentId),
|
||||
);
|
||||
const hasOlder =
|
||||
useSessionStore((state) => state.sessions[serverId]?.agentTimelineHasOlder.get(agentId)) ===
|
||||
true;
|
||||
const isLoadingOlder =
|
||||
useSessionStore((state) =>
|
||||
state.sessions[serverId]?.agentTimelineOlderFetchInFlight.get(agentId),
|
||||
) === true;
|
||||
const setOlderFetchInFlight = useSessionStore(
|
||||
(state) => state.setAgentTimelineOlderFetchInFlight,
|
||||
);
|
||||
|
||||
const setInFlight = useCallback(
|
||||
(value: boolean) => {
|
||||
setOlderFetchInFlight(serverId, (prev) => {
|
||||
if (prev.get(agentId) === value) {
|
||||
return prev;
|
||||
}
|
||||
const next = new Map(prev);
|
||||
next.set(agentId, value);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
[agentId, serverId, setOlderFetchInFlight],
|
||||
);
|
||||
|
||||
const loadOlder = useCallback(() => {
|
||||
const latestSession = useSessionStore.getState().sessions[serverId];
|
||||
const latestIsLoading = latestSession?.agentTimelineOlderFetchInFlight.get(agentId) === true;
|
||||
if (!client || !cursor || !hasOlder || isLoadingOlder || latestIsLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInFlight(true);
|
||||
void client
|
||||
.fetchAgentTimeline(agentId, {
|
||||
direction: "before",
|
||||
cursor: { epoch: cursor.epoch, seq: cursor.startSeq },
|
||||
limit: TIMELINE_FETCH_PAGE_SIZE,
|
||||
projection: "canonical",
|
||||
})
|
||||
.catch((error) => {
|
||||
console.warn("[Timeline] failed to load older agent history", agentId, error);
|
||||
toast?.show("Couldn't load older history", {
|
||||
durationMs: 2200,
|
||||
testID: "agent-load-older-history-toast",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
setInFlight(false);
|
||||
});
|
||||
}, [agentId, client, cursor, hasOlder, isLoadingOlder, serverId, setInFlight, toast]);
|
||||
|
||||
return {
|
||||
isLoadingOlder,
|
||||
hasOlder,
|
||||
loadOlder,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user