mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-24 10:12:44 +00:00
fix(app): preserve timeline retry backoff (#3736)
Equivalent visibility publications were acting as implicit retry commands, so unrelated tab and layout changes could restart failed timeline work. Keep visibility identity stable and reserve immediate retries for explicit user action.
This commit is contained in:
@@ -49,7 +49,7 @@ A failed catch-up or subscription reconcile retries on its own, doubling from 1s
|
||||
A fixed 1s retry turned a persistent daemon-side refusal — a Codex thread that already has an active
|
||||
writer, say — into a request and a log line every second on an idle app. The delay resets on success,
|
||||
reconnect, delivery-mode change, and visibility change, so recovery is still immediate once the
|
||||
condition clears.
|
||||
condition clears. Republishing the same visibility set is a no-op and never bypasses backoff.
|
||||
|
||||
Background retries are silent. The retry the user presses in the sync-error callout is a fallible
|
||||
user action and owns its pending state: `retrying` is a status the sync model publishes, not a React
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useRef } from "react";
|
||||
import { collectAllPanes, type WorkspaceLayout } from "@/stores/workspace-layout-store";
|
||||
import type { WorkspaceTab } from "@/workspace-tabs/model";
|
||||
import { deriveWorkspacePaneState } from "./workspace-pane-state";
|
||||
@@ -25,3 +26,20 @@ export function selectVisibleAgentIds(input: {
|
||||
),
|
||||
].sort();
|
||||
}
|
||||
|
||||
export function useVisibleAgentIds(input: {
|
||||
layout: WorkspaceLayout | null;
|
||||
tabs: WorkspaceTab[];
|
||||
routeFocused: boolean;
|
||||
focusedPaneOnly: boolean;
|
||||
}): string[] {
|
||||
const nextAgentIds = selectVisibleAgentIds(input);
|
||||
const stableAgentIds = useRef<string[]>([]);
|
||||
if (
|
||||
stableAgentIds.current.length !== nextAgentIds.length ||
|
||||
stableAgentIds.current.some((agentId, index) => agentId !== nextAgentIds[index])
|
||||
) {
|
||||
stableAgentIds.current = nextAgentIds;
|
||||
}
|
||||
return stableAgentIds.current;
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ import type {
|
||||
} from "@/keyboard/keyboard-action-dispatcher";
|
||||
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
||||
import { normalizeWorkspaceTabTarget, workspaceTabTargetsEqual } from "@/workspace-tabs/identity";
|
||||
import { selectVisibleAgentIds } from "./visible-agent-ids";
|
||||
import { useVisibleAgentIds } from "./visible-agent-ids";
|
||||
import {
|
||||
getHostRuntimeStore,
|
||||
useHostRuntimeClient,
|
||||
@@ -1845,16 +1845,16 @@ function WorkspaceScreenContent({
|
||||
const viewedTimelineSync = useSessionStore(
|
||||
(state) => state.sessions[normalizedServerId]?.viewedTimelineSync ?? null,
|
||||
);
|
||||
const visibleAgentIds = useMemo(
|
||||
() =>
|
||||
selectVisibleAgentIds({
|
||||
layout: workspaceLayout,
|
||||
tabs: uiTabs,
|
||||
routeFocused: isRouteFocused,
|
||||
focusedPaneOnly: isMobile || isFocusModeEnabled || !supportsDesktopPaneSplits(),
|
||||
}),
|
||||
[isFocusModeEnabled, isMobile, isRouteFocused, uiTabs, workspaceLayout],
|
||||
const syncFocusedPaneOnly = useMemo(
|
||||
() => isMobile || isFocusModeEnabled || !supportsDesktopPaneSplits(),
|
||||
[isFocusModeEnabled, isMobile],
|
||||
);
|
||||
const visibleAgentIds = useVisibleAgentIds({
|
||||
layout: workspaceLayout,
|
||||
tabs: uiTabs,
|
||||
routeFocused: isRouteFocused,
|
||||
focusedPaneOnly: syncFocusedPaneOnly,
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
if (!persistenceKey || !viewedTimelineSync) {
|
||||
return;
|
||||
|
||||
@@ -464,6 +464,24 @@ test("manual retries can immediately re-attempt a failed catch-up", async () =>
|
||||
await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"));
|
||||
});
|
||||
|
||||
test("redeclaring unchanged visibility does not bypass catch-up backoff", async () => {
|
||||
const world = new TimelineWorld();
|
||||
world.sync.setConnected(true);
|
||||
world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]);
|
||||
const membership = await world.nextMembership();
|
||||
membership.succeed();
|
||||
|
||||
const failed = await world.nextFetch("agent-a");
|
||||
failed.fail("timeline unavailable");
|
||||
await world.nextRetry();
|
||||
|
||||
world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]);
|
||||
|
||||
world.expectNoPendingMembership();
|
||||
world.expectNoPendingFetch();
|
||||
expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("error");
|
||||
});
|
||||
|
||||
test("a manual retry that fails returns to the error state", async () => {
|
||||
const world = new TimelineWorld();
|
||||
world.sync.setConnected(true);
|
||||
@@ -586,6 +604,22 @@ test("membership failures retry with exponential backoff", async () => {
|
||||
await vi.waitFor(() => expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("ready"));
|
||||
});
|
||||
|
||||
test("redeclaring unchanged visibility does not bypass membership backoff", async () => {
|
||||
const world = new TimelineWorld();
|
||||
world.sync.setConnected(true);
|
||||
world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]);
|
||||
|
||||
const failed = await world.nextMembership();
|
||||
failed.fail("subscription unavailable");
|
||||
await world.nextRetry();
|
||||
|
||||
world.sync.replaceVisibleAgentIds("workspace", ["agent-a"]);
|
||||
|
||||
world.expectNoPendingMembership();
|
||||
world.expectNoPendingFetch();
|
||||
expect(world.sync.getAgentTimelineStatus("agent-a")).toBe("error");
|
||||
});
|
||||
|
||||
test("backgrounding preserves the hot membership without catch-up on return", async () => {
|
||||
const world = new TimelineWorld();
|
||||
world.sync.setConnected(true);
|
||||
|
||||
@@ -365,12 +365,6 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed
|
||||
}
|
||||
};
|
||||
|
||||
const retryFailedCatchUps = () => {
|
||||
for (const agentId of acknowledged) {
|
||||
if (catchUps.get(agentId)?.status === "error") startCatchUp(agentId);
|
||||
}
|
||||
};
|
||||
|
||||
const retryVisibleAgentTimeline = (agentId: string) => {
|
||||
if (!isDesired(agentId) || manualRetries.has(agentId)) return;
|
||||
const catchUp = catchUps.get(agentId);
|
||||
@@ -407,8 +401,6 @@ export function createViewedTimelineSync(ports: ViewedTimelineSyncPorts): Viewed
|
||||
}
|
||||
if (sameAgentIds(nextDesired, desired)) {
|
||||
if (statusChanged) notifyListeners();
|
||||
if (deliveryMode === "selective" && membershipNeedsRetry) void reconcileMembership();
|
||||
retryFailedCatchUps();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user