Files
paseo/packages/app/src/hooks/use-load-older-agent-history.ts
T
654439005a 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>
2026-05-02 19:53:50 +08:00

77 lines
2.3 KiB
TypeScript

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,
};
}