From a3d751f6e7aee059c4ad8b289037c82fbb391322 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 14 Sep 2026 18:37:59 -0400 Subject: [PATCH] fix(settings): treat a host-too-old status read as an outdated server --- .../settings/SessionHistoryServerRow.tsx | 16 +++++++--------- .../settings/session-history-status-copy.ts | 5 +++++ .../settings/use-session-search-status.test.tsx | 15 +++++++++++++++ .../settings/use-session-search-status.ts | 16 +++++++++++----- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/renderer/src/components/settings/SessionHistoryServerRow.tsx b/src/renderer/src/components/settings/SessionHistoryServerRow.tsx index b69b7a23830..acb99ed0eb3 100644 --- a/src/renderer/src/components/settings/SessionHistoryServerRow.tsx +++ b/src/renderer/src/components/settings/SessionHistoryServerRow.tsx @@ -12,6 +12,7 @@ import { } from './runtime-environment-host-details' import { SessionHistoryComputerRow } from './SessionHistoryComputerRow' import { + isHostTooOldError, sessionSearchCheckingMessage, sessionSearchReadErrorMessage, sessionSearchStatusDetails, @@ -19,11 +20,6 @@ import { } from './session-history-status-copy' import { useSessionSearchStatus } from './use-session-search-status' -// IPC wraps a rejection's message, so the host-too-old marker arrives inside a longer string. -function isHostTooOldError(error: unknown): boolean { - return error instanceof Error && error.message.includes('host-too-old') -} - export function SessionHistoryServerRow({ environment, details, @@ -38,14 +34,16 @@ export function SessionHistoryServerRow({ const mounted = useMountedRef() const openSettingsPage = useAppStore((state) => state.openSettingsPage) const openSettingsTarget = useAppStore((state) => state.openSettingsTarget) - const [tooOld, setTooOld] = useState(false) + const [tooOldOnSet, setTooOldOnSet] = useState(false) const [busy, setBusy] = useState(false) const connectionState = getRuntimeServerConnectionState(details) const connected = isRuntimeServerTransportConnected(connectionState) - const { status, failed, adopt } = useSessionSearchStatus({ + const { status, failed, hostTooOld, adopt } = useSessionSearchStatus({ executionHostId: hostId, - active: connected && !tooOld + active: connected && !tooOldOnSet }) + // A status read or a set call can each prove the server predates session search. + const tooOld = tooOldOnSet || hostTooOld const enabled = status?.enabled === true async function setEnabled(next: boolean): Promise { @@ -58,7 +56,7 @@ export function SessionHistoryServerRow({ return } if (isHostTooOldError(error)) { - setTooOld(true) + setTooOldOnSet(true) return } onError( diff --git a/src/renderer/src/components/settings/session-history-status-copy.ts b/src/renderer/src/components/settings/session-history-status-copy.ts index c4d834b670e..45de302ee62 100644 --- a/src/renderer/src/components/settings/session-history-status-copy.ts +++ b/src/renderer/src/components/settings/session-history-status-copy.ts @@ -94,3 +94,8 @@ export function sessionSearchOffMessage(): string { export function sessionSearchReadErrorMessage(): string { return translate('sessionHistory.status.error', 'Could not read index status. Retrying…') } + +// IPC wraps a rejection's message, so the host-too-old marker arrives inside a longer string. +export function isHostTooOldError(error: unknown): boolean { + return error instanceof Error && error.message.includes('host-too-old') +} diff --git a/src/renderer/src/components/settings/use-session-search-status.test.tsx b/src/renderer/src/components/settings/use-session-search-status.test.tsx index 179612e38bd..b1d5558cc99 100644 --- a/src/renderer/src/components/settings/use-session-search-status.test.tsx +++ b/src/renderer/src/components/settings/use-session-search-status.test.tsx @@ -235,3 +235,18 @@ it('does not overlap slow status requests and stops polling on unmount', async ( }) expect(mocks.status).toHaveBeenCalledTimes(beforeUnmount) }) + +it('marks the host too old on a host-too-old rejection and stops polling', async () => { + mocks.status.mockRejectedValue(new Error('Error invoking remote method: host-too-old')) + const { result } = poll() + await act(async () => { + await vi.advanceTimersByTimeAsync(0) + }) + expect(result.current.hostTooOld).toBe(true) + expect(result.current.failed).toBe(true) + const calls = mocks.status.mock.calls.length + await act(async () => { + await vi.advanceTimersByTimeAsync(30_000) + }) + expect(mocks.status.mock.calls.length).toBe(calls) +}) diff --git a/src/renderer/src/components/settings/use-session-search-status.ts b/src/renderer/src/components/settings/use-session-search-status.ts index 717397314f3..1e8bf25e000 100644 --- a/src/renderer/src/components/settings/use-session-search-status.ts +++ b/src/renderer/src/components/settings/use-session-search-status.ts @@ -3,11 +3,13 @@ import type { AiVaultSearchStatus } from '../../../../shared/ai-vault-search-typ import type { ExecutionHostId } from '../../../../shared/execution-host' import { useWindowStreamVisible } from '@/hooks/use-window-stream-visibility' import { installWindowVisibilityInterval } from '@/lib/window-visibility-interval' -import { sessionSearchPollIntervalMs } from './session-history-status-copy' +import { isHostTooOldError, sessionSearchPollIntervalMs } from './session-history-status-copy' export type SessionSearchStatusRead = { status: AiVaultSearchStatus | null failed: boolean + /** The host answered that it has no session search at all; polling stops. */ + hostTooOld: boolean /** Adopt a status the caller already holds, e.g. the answer to a set call. */ adopt: (status: AiVaultSearchStatus) => void } @@ -27,6 +29,7 @@ export function useSessionSearchStatus(args: { const visible = useWindowStreamVisible(0) const [status, setStatus] = useState(null) const [failed, setFailed] = useState(false) + const [hostTooOld, setHostTooOld] = useState(false) const intervalMs = sessionSearchPollIntervalMs(status) const adopt = useCallback((next: AiVaultSearchStatus) => { setStatus(next) @@ -38,7 +41,7 @@ export function useSessionSearchStatus(args: { setFailed(false) return } - if (!visible) { + if (!visible || hostTooOld) { return } let disposed = false @@ -56,10 +59,13 @@ export function useSessionSearchStatus(args: { setStatus(next) setFailed(false) } - } catch { + } catch (error) { if (!disposed) { setStatus(null) setFailed(true) + if (isHostTooOldError(error)) { + setHostTooOld(true) + } } } finally { inFlight = false @@ -70,7 +76,7 @@ export function useSessionSearchStatus(args: { disposed = true stopPolling() } - }, [executionHostId, active, visible, refresh, intervalMs]) + }, [executionHostId, active, visible, refresh, intervalMs, hostTooOld]) - return { status, failed, adopt } + return { status, failed, hostTooOld, adopt } }