fix(settings): treat a host-too-old status read as an outdated server

This commit is contained in:
Jinwoo-H
2026-09-14 18:37:59 -04:00
parent 0497e6fe93
commit a3d751f6e7
4 changed files with 38 additions and 14 deletions
@@ -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<void> {
@@ -58,7 +56,7 @@ export function SessionHistoryServerRow({
return
}
if (isHostTooOldError(error)) {
setTooOld(true)
setTooOldOnSet(true)
return
}
onError(
@@ -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')
}
@@ -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)
})
@@ -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<AiVaultSearchStatus | null>(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 }
}