@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>
77 lines
2.3 KiB
TypeScript
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,
|
|
};
|
|
}
|