fix(runtime): hold four more host reads through an unverifiable probe (#20095)

* fix(runtime): hold the session mirror through an unverifiable probe

Two derivations read the same host state and reached opposite verdicts, and
the destructive one won. When a status probe came back unverifiable over a
still-ready transport, runtimeHostConnectionStateForEntry called the host
'runtime-unavailable' (connected) while getReachableRuntimeSessionMirrorTargets
dropped it, tearing down and cold-rebuilding the session-tab mirror while the
host's flows were still delivering.

The root cause is that applyRuntimeHostStatusSnapshot nulls entry.status for any
non-verified probe while the snapshot retains the runtime identity. The
connection-state reader consults the snapshot; the mirror-target reader did not.

Give both readers one answer:

- lastVerifiedRuntimeStatus() in shared/runtime-host-status.ts is now the single
  definition of "the last identity the host answered with". runtime-status.ts
  already had this inline as previousVerifiedStatus and now calls it.
- The mirror-target reader asks the shared connection verdict instead of
  entry.status, gated on isDisconnectedRuntimeHostState -- only the one exit
  verdict earns a destructive read, per docs/reference/ssh-execution-boundary.md.
  'checking' and 'reconnecting' are unverifiable, not evidence of an exit.

Holding the target through the outage would strand the mirror on its own: the
subscription is installed by the effect in use-web-session-tabs-sync.ts keyed on
useRuntimeSessionMirrorEnvironmentKey(), a stream 'end' frame is dropped without
resubscribing, and the parking layer retries only a rejected subscribe call. The
teardown was the recovery. So regaining contact now advances the connection
epoch, giving recovery its own "the host is back" trigger rather than leaving the
mirror to be restored as a side effect of having been destroyed.

The connection epoch is not the runtime session: a same-runtime return fires no
restart hook, no provider session bump, and no toast.

* test(runtime): drop the redundant status casts the new casting gate rejects

* fix(runtime): hold four more host reads through an unverifiable probe

Siblings of the session-mirror defect fixed in #20085. runtime-status-snapshot
nulls `entry.status` for any non-verified probe while the snapshot retains the
host's identity, so a host with a ready transport that is still delivering reads
as gone to anything gating on `entry.status != null`.

- client-event subscription selection dropped the stream for such a host, and
  its disconnect edge bumped the SSH generation, rebuilding even the active
  host's subscription
- the web client's active session-tabs stream tore down and cold-rebuilt, twice
  per blip
- landing preflight discarded its whole result
- runtime-aware SSH selectors blanked mirrored target rows

Each now reads the shared verdict, isConnectedRuntimeHostState of
runtimeHostConnectionStateForEntry, or lastVerifiedRuntimeStatus where the read
is host identity rather than reachability. No new predicate: every
"genuinely gone" case is byte-identical, so nothing gains a retry loop.

* test(runtime): build real host statuses instead of casting partials
This commit is contained in:
Neil
2026-09-17 02:02:48 -07:00
committed by GitHub
parent 67dda9affe
commit 182ff17141
7 changed files with 489 additions and 9 deletions
@@ -1,6 +1,10 @@
import { useEffect, useMemo } from 'react'
import { useAppStore } from '../store'
import { installWindowVisibilityInterval } from '@/lib/window-visibility-interval'
import {
isConnectedRuntimeHostState,
runtimeHostConnectionStateForEntry
} from '@/runtime/runtime-host-connection-state'
import {
getLandingPreflightIssues,
hasGitHubBackedProject,
@@ -18,10 +22,13 @@ export function useLandingPreflightRuntime(): { preflightIssues: PreflightIssue[
return 'local'
}
const runtimeStatus = s.runtimeStatusByEnvironmentId.get(environmentId)
// Why the shared verdict and not `entry.status`: an unverifiable probe nulls it while the
// transport is still up, and reading that as unreachable discarded the whole preflight
// result for a host that never went away (docs/reference/ssh-execution-boundary.md).
const reachability = runtimeStatus
? runtimeStatus.status === null
? 'unreachable'
: 'reachable'
? isConnectedRuntimeHostState(runtimeHostConnectionStateForEntry(runtimeStatus))
? 'reachable'
: 'unreachable'
: 'unknown'
return `${environmentId}:${runtimeStatus?.connectionGeneration ?? 0}:${reachability}`
})
@@ -0,0 +1,102 @@
// @vitest-environment happy-dom
import { act, cleanup, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { RuntimeHostStatusSnapshot } from '../../../shared/runtime-host-status'
import type { RuntimeStatus } from '../../../shared/runtime-types'
import { useAppStore } from '../store'
import type { RuntimeEnvironmentStatus } from '../store/slices/runtime-status-types'
import type { AppState } from '../store/types'
import { useLandingPreflightRuntime } from './landing-preflight-runtime'
const ENVIRONMENT_ID = 'environment-a'
const initialState = useAppStore.getInitialState()
const invalidate = vi.fn()
const refresh = vi.fn(async () => {})
function makeStatus(): RuntimeStatus {
return {
runtimeId: 'runtime-a',
rendererGraphEpoch: 0,
graphStatus: 'ready',
authoritativeWindowId: null,
liveTabCount: 0,
liveLeafCount: 0
}
}
function snapshot(overrides: Partial<RuntimeHostStatusSnapshot> = {}): RuntimeHostStatusSnapshot {
return {
environmentId: ENVIRONMENT_ID,
pairingRevision: 1,
sequence: 1,
checkedAt: 1,
status: makeStatus(),
verification: 'verified',
transport: 'ready',
...overrides
}
}
function setStatusEntry(entry: RuntimeEnvironmentStatus): void {
useAppStore.setState({
runtimeStatusByEnvironmentId: new Map([[ENVIRONMENT_ID, entry]])
})
}
describe('landing preflight under an unverifiable host probe', () => {
beforeEach(() => {
invalidate.mockClear()
refresh.mockClear()
useAppStore.setState(
{
...initialState,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook under test reads only activeRuntimeEnvironmentId; the rest of GlobalSettings never reaches it.
settings: { activeRuntimeEnvironmentId: ENVIRONMENT_ID } as AppState['settings'],
invalidatePreflightStatus: invalidate,
refreshPreflightStatus: refresh
},
true
)
setStatusEntry({ status: makeStatus(), snapshot: snapshot(), checkedAt: 1 })
})
afterEach(() => {
cleanup()
useAppStore.setState(initialState, true)
})
it('keeps preflight state when a ready host answers an unverifiable probe', () => {
renderHook(() => useLandingPreflightRuntime())
expect(invalidate).not.toHaveBeenCalled()
act(() => {
setStatusEntry({
status: null,
snapshot: snapshot({ sequence: 2, checkedAt: 2, verification: 'unavailable' }),
checkedAt: 2
})
})
expect(invalidate).not.toHaveBeenCalled()
})
it('still discards preflight state once the transport goes down', () => {
renderHook(() => useLandingPreflightRuntime())
act(() => {
setStatusEntry({
status: null,
snapshot: snapshot({
sequence: 2,
checkedAt: 2,
verification: 'unavailable',
transport: 'disconnected'
}),
checkedAt: 2
})
})
expect(invalidate).toHaveBeenCalled()
})
})
@@ -1,4 +1,8 @@
import { getRuntimeEnvironmentRevision } from '@/runtime/runtime-environment-revision'
import {
isConnectedRuntimeHostState,
runtimeHostConnectionStateForEntry
} from '@/runtime/runtime-host-connection-state'
import { getEnvironmentSshStateGeneration } from '@/store/slices/runtime-environment-ssh'
import { getRuntimeEnvironmentConnectionGeneration } from '@/store/slices/runtime-status'
import type { AppState } from '../../store/types'
@@ -21,19 +25,34 @@ export function getRuntimeClientEventEnvironmentIds(
ids.add(activeEnvironmentId)
}
for (const environment of state.runtimeEnvironments ?? []) {
if (state.runtimeStatusByEnvironmentId?.get(environment.id)?.status) {
if (isRuntimeHostStillInContact(state, environment.id)) {
ids.add(environment.id)
}
}
return [...ids]
}
/**
* Why the shared verdict and not `entry.status`: an unverifiable probe nulls `entry.status`
* while the transport stays up and the host keeps delivering. Reading that as "gone" dropped
* the client-event subscription and fired the disconnect edge on a live host. Contact is lost
* only once the transport itself says so (docs/reference/ssh-execution-boundary.md).
*/
function isRuntimeHostStillInContact(
state: RuntimeEnvironmentStoreSyncState,
environmentId: string
): boolean {
return isConnectedRuntimeHostState(
runtimeHostConnectionStateForEntry(state.runtimeStatusByEnvironmentId?.get(environmentId))
)
}
export function getReachableRuntimeEnvironmentIds(
state: RuntimeEnvironmentStoreSyncState
): string[] {
const ids: string[] = []
for (const [environmentId, status] of state.runtimeStatusByEnvironmentId ?? []) {
if (status?.status) {
for (const environmentId of state.runtimeStatusByEnvironmentId?.keys() ?? []) {
if (isRuntimeHostStillInContact(state, environmentId)) {
ids.push(environmentId)
}
}
@@ -0,0 +1,149 @@
import { describe, expect, it } from 'vitest'
import type { RuntimeHostStatusSnapshot } from '../../../shared/runtime-host-status'
import type { RuntimeStatus } from '../../../shared/runtime-types'
import {
getReachableRuntimeEnvironmentIds,
getRuntimeClientEventEnvironmentIds
} from '@/hooks/ipc-events/runtime-environment-subscription-selection'
import type { RuntimeEnvironmentStoreSyncState } from '@/hooks/ipc-events/runtime-environment-subscription-selection'
import {
selectRuntimeAwareSshError,
selectRuntimeAwareSshStatus
} from '@/store/slices/runtime-environment-ssh-selectors'
const ENVIRONMENT_ID = 'environment-a'
const VERIFIED_STATUS: RuntimeStatus = {
runtimeId: 'runtime-a',
rendererGraphEpoch: 0,
graphStatus: 'ready',
authoritativeWindowId: null,
liveTabCount: 0,
liveLeafCount: 0
}
function snapshot(overrides: Partial<RuntimeHostStatusSnapshot> = {}): RuntimeHostStatusSnapshot {
return {
environmentId: ENVIRONMENT_ID,
pairingRevision: 1,
sequence: 2,
checkedAt: 2,
status: VERIFIED_STATUS,
verification: 'verified',
transport: 'ready',
...overrides
}
}
/** The defect shape: the host answered once, its transport is still up, the last probe did not answer. */
function unverifiableWhileReady(): {
status: null
checkedAt: number
snapshot: RuntimeHostStatusSnapshot
} {
return { status: null, checkedAt: 2, snapshot: snapshot({ verification: 'unavailable' }) }
}
function transportDown(): { status: null; checkedAt: number; snapshot: RuntimeHostStatusSnapshot } {
return {
status: null,
checkedAt: 2,
snapshot: snapshot({ verification: 'unavailable', transport: 'disconnected' })
}
}
function syncState(
entry: {
status: RuntimeStatus | null
checkedAt: number
snapshot?: RuntimeHostStatusSnapshot
} | null
): RuntimeEnvironmentStoreSyncState {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the readers under test consult only the four fields below; the rest of AppState never reaches them.
return {
runtimeEnvironments: [{ id: ENVIRONMENT_ID, createdAt: 1 }],
runtimeStatusByEnvironmentId: entry ? new Map([[ENVIRONMENT_ID, entry]]) : new Map(),
settings: { activeRuntimeEnvironmentId: null },
sshStateByEnvironment: new Map()
} as unknown as RuntimeEnvironmentStoreSyncState
}
describe('runtime client-event subscription selection', () => {
it('keeps a host whose transport is ready but whose last probe went unverifiable', () => {
expect(getRuntimeClientEventEnvironmentIds(syncState(unverifiableWhileReady()))).toEqual([
ENVIRONMENT_ID
])
})
it('keeps that host in the reachable set, so no spurious disconnect edge fires', () => {
expect(getReachableRuntimeEnvironmentIds(syncState(unverifiableWhileReady()))).toEqual([
ENVIRONMENT_ID
])
})
it('still drops a host whose transport went down', () => {
expect(getRuntimeClientEventEnvironmentIds(syncState(transportDown()))).toEqual([])
expect(getReachableRuntimeEnvironmentIds(syncState(transportDown()))).toEqual([])
})
it('still drops a retired host and one that never answered', () => {
const retired = { status: null, checkedAt: 2, snapshot: snapshot({ retired: true }) }
expect(getRuntimeClientEventEnvironmentIds(syncState(retired))).toEqual([])
expect(
getRuntimeClientEventEnvironmentIds(
syncState({
status: null,
checkedAt: 0,
snapshot: snapshot({ status: null, verification: 'checking' })
})
)
).toEqual([])
expect(getRuntimeClientEventEnvironmentIds(syncState(null))).toEqual([])
})
it('keeps a verified host', () => {
expect(
getRuntimeClientEventEnvironmentIds(
syncState({ status: VERIFIED_STATUS, checkedAt: 2, snapshot: snapshot() })
)
).toEqual([ENVIRONMENT_ID])
})
})
describe('runtime-aware SSH selectors', () => {
function sshState(entry: {
status: RuntimeStatus | null
checkedAt: number
snapshot?: RuntimeHostStatusSnapshot
}) {
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the selectors read only the SSH maps and the status map built below.
return {
sshConnectionStates: new Map(),
sshTargetLabels: new Map(),
removedSshTargetLabels: new Map(),
sshTargetsHydrated: true,
sshStateByEnvironment: new Map([
[
ENVIRONMENT_ID,
{
targetsHydrated: true,
connectionStates: new Map([['target-a', { status: 'connected', error: 'boom' }]]),
targetLabels: new Map([['target-a', 'Target A']]),
removedTargetLabels: new Map()
}
]
]),
runtimeStatusByEnvironmentId: new Map([[ENVIRONMENT_ID, entry]])
} as unknown as Parameters<typeof selectRuntimeAwareSshStatus>[0]
}
it('keeps reporting a mirrored SSH target while the host probe is unverifiable', () => {
const state = sshState(unverifiableWhileReady())
expect(selectRuntimeAwareSshStatus(state, ENVIRONMENT_ID, 'target-a')).toBe('connected')
expect(selectRuntimeAwareSshError(state, ENVIRONMENT_ID, 'target-a')).toBe('boom')
})
it('still withholds SSH state once the transport is down', () => {
const state = sshState(transportDown())
expect(selectRuntimeAwareSshStatus(state, ENVIRONMENT_ID, 'target-a')).toBeNull()
})
})
@@ -0,0 +1,190 @@
// @vitest-environment happy-dom
import { act, cleanup, renderHook } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { RuntimeHostStatusSnapshot } from '../../../shared/runtime-host-status'
import type { RuntimeStatus } from '../../../shared/runtime-types'
import type { PublicKnownRuntimeEnvironment } from '../../../shared/runtime-environments'
import type * as WorktreeRuntimeOwnerModule from '@/lib/worktree-runtime-owner'
const mocks = vi.hoisted(() => ({
getExplicitRuntimeEnvironmentIdForWorktree: vi.fn(),
runtimeSessionMirrorEnvironmentKey: vi.fn()
}))
vi.mock('./use-runtime-session-mirror-environment-key', () => ({
useRuntimeSessionMirrorEnvironmentKey: mocks.runtimeSessionMirrorEnvironmentKey
}))
vi.mock('@/lib/worktree-runtime-owner', async (importOriginal) => {
const actual = await importOriginal<typeof WorktreeRuntimeOwnerModule>()
return {
...actual,
getExplicitRuntimeEnvironmentIdForWorktree: mocks.getExplicitRuntimeEnvironmentIdForWorktree
}
})
import { useAppStore } from '@/store'
import type { RuntimeEnvironmentStatus } from '@/store/slices/runtime-status-types'
import { replaceRuntimeEnvironmentRevisions } from './runtime-environment-revision'
import { clearHostLiveTerminalProbesForTests } from './host-live-terminal-probe'
import {
resetWebSessionTabsSnapshotFreshnessForTests,
useWebSessionTabsSync
} from './web-session-tabs-sync'
import { clearRuntimeEnvironmentConnectionGenerationsForTests } from '@/store/slices/runtime-status'
const ENV_A = 'env-a'
const WORKTREE = 'repo-a::worktree-a'
const REVISION_A = 101
const MIRROR_KEY = `${ENV_A}runtime-a0${REVISION_A}`
const initialState = useAppStore.getInitialState()
type RuntimeSubscribe = typeof window.api.runtimeEnvironments.subscribe
type RuntimeSubscription = {
request: Parameters<RuntimeSubscribe>[0]
unsubscribe: ReturnType<typeof vi.fn>
}
const subscriptions: RuntimeSubscription[] = []
const runtimeCall = vi.fn(async (_args: { method: string }) => ({
id: 'list-all',
ok: true as const,
result: { snapshots: [] },
_meta: { runtimeId: 'runtime-a' }
}))
const runtimeSubscribe = vi.fn<RuntimeSubscribe>(async (request) => {
const unsubscribe = vi.fn()
subscriptions.push({ request, unsubscribe })
return { unsubscribe, sendBinary: vi.fn() }
})
async function settle(): Promise<void> {
await Promise.resolve()
await Promise.resolve()
await Promise.resolve()
}
function makeStatus(runtimeId: string): RuntimeStatus {
return {
runtimeId,
rendererGraphEpoch: 0,
graphStatus: 'ready',
authoritativeWindowId: null,
liveTabCount: 0,
liveLeafCount: 0
}
}
function verifiedSnapshot(): RuntimeHostStatusSnapshot {
return {
environmentId: ENV_A,
pairingRevision: REVISION_A,
sequence: 1,
checkedAt: 1,
status: makeStatus('runtime-a'),
verification: 'verified',
transport: 'ready'
}
}
function setRuntimeStatusEntry(entry: RuntimeEnvironmentStatus): void {
useAppStore.setState({ runtimeStatusByEnvironmentId: new Map([[ENV_A, entry]]) })
}
function activeTabsSubscriptions(): RuntimeSubscription[] {
return subscriptions.filter(({ request }) => request.method === 'session.tabs.subscribe')
}
describe('useWebSessionTabsSync under an unverifiable host probe', () => {
beforeEach(() => {
subscriptions.length = 0
runtimeCall.mockClear()
runtimeSubscribe.mockClear()
mocks.getExplicitRuntimeEnvironmentIdForWorktree.mockReset().mockReturnValue(ENV_A)
mocks.runtimeSessionMirrorEnvironmentKey.mockReset().mockReturnValue(MIRROR_KEY)
Object.defineProperty(window, 'api', {
configurable: true,
value: { runtimeEnvironments: { call: runtimeCall, subscribe: runtimeSubscribe } }
})
resetWebSessionTabsSnapshotFreshnessForTests()
clearHostLiveTerminalProbesForTests()
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the mirror scan and revision ledger read only id, createdAt and pairingRevision.
const runtimeEnvironments = [
{ id: ENV_A, createdAt: 100, pairingRevision: REVISION_A }
] as PublicKnownRuntimeEnvironment[]
replaceRuntimeEnvironmentRevisions(runtimeEnvironments)
useAppStore.setState(
{
...initialState,
activeWorktreeId: WORKTREE,
workspaceSessionReady: true,
runtimeEnvironments,
runtimeStatusByEnvironmentId: new Map([
[
ENV_A,
{
status: makeStatus('runtime-a'),
snapshot: verifiedSnapshot(),
checkedAt: 1,
connectionGeneration: 1
}
]
])
},
true
)
})
afterEach(() => {
cleanup()
useAppStore.setState(initialState, true)
replaceRuntimeEnvironmentRevisions([])
resetWebSessionTabsSnapshotFreshnessForTests()
clearRuntimeEnvironmentConnectionGenerationsForTests()
})
it('holds the active session-tabs subscription when the probe goes unverifiable', async () => {
renderHook(() => useWebSessionTabsSync())
await act(settle)
const held = activeTabsSubscriptions()
expect(held).toHaveLength(1)
// The transport is still ready and the host is still delivering; only the probe failed.
await act(async () => {
setRuntimeStatusEntry({
status: null,
snapshot: { ...verifiedSnapshot(), sequence: 2, checkedAt: 2, verification: 'unavailable' },
checkedAt: 2,
connectionGeneration: 1
})
await settle()
})
expect(held[0]!.unsubscribe).not.toHaveBeenCalled()
expect(activeTabsSubscriptions()).toHaveLength(1)
})
it('still restarts the subscription when the host answers with a replacement runtime', async () => {
renderHook(() => useWebSessionTabsSync())
await act(settle)
expect(activeTabsSubscriptions()).toHaveLength(1)
await act(async () => {
setRuntimeStatusEntry({
status: makeStatus('runtime-b'),
snapshot: {
...verifiedSnapshot(),
sequence: 2,
checkedAt: 2,
status: makeStatus('runtime-b')
},
checkedAt: 2,
connectionGeneration: 1
})
await settle()
})
expect(activeTabsSubscriptions()).toHaveLength(2)
})
})
@@ -1,4 +1,5 @@
import { useEffect, useLayoutEffect, useRef } from 'react'
import { lastVerifiedRuntimeStatus } from '../../../../shared/runtime-host-status'
import { useAppStore } from '../../store'
import { getExplicitRuntimeEnvironmentIdForWorktree } from '../../lib/worktree-runtime-owner'
import { useRuntimeSessionMirrorEnvironmentKey } from '../use-runtime-session-mirror-environment-key'
@@ -35,11 +36,14 @@ export function useWebSessionTabsSync(): void {
getExplicitRuntimeEnvironmentIdForWorktree(state, state.activeWorktreeId)
)
// Keep this subscription dependency: a runtime reconnect can retain the same environment id
// while replacing its runtime instance, which must restart the scoped stream.
// while replacing its runtime instance, which must restart the scoped stream. Read the last
// identity the host answered with, not `entry.status` — an unverifiable probe nulls that and
// cold-rebuilt this stream for a host that was still delivering.
const activeWorktreeRuntimeId = useAppStore((state) => {
const environmentId = getExplicitRuntimeEnvironmentIdForWorktree(state, state.activeWorktreeId)
return environmentId
? (state.runtimeStatusByEnvironmentId.get(environmentId)?.status?.runtimeId ?? null)
? (lastVerifiedRuntimeStatus(state.runtimeStatusByEnvironmentId.get(environmentId))
?.runtimeId ?? null)
: null
})
const activeWorktreeRuntimeConnectionGeneration = useAppStore((state) => {
@@ -1,5 +1,9 @@
import type { AppState } from '../types'
import type { SshConnectionStatus } from '../../../../shared/ssh-types'
import {
isConnectedRuntimeHostState,
runtimeHostConnectionStateForEntry
} from '@/runtime/runtime-host-connection-state'
type RuntimeAwareSshReadState = Pick<
AppState,
@@ -11,8 +15,13 @@ type RuntimeAwareSshReadState = Pick<
> &
Partial<Pick<AppState, 'runtimeStatusByEnvironmentId'>>
// Why the shared verdict and not `entry.status`: an unverifiable probe nulls it while the
// transport is still up, and blanking the mirrored SSH rows of a host that never went away
// reads as "the targets vanished" (docs/reference/ssh-execution-boundary.md).
function isEnvironmentReachable(state: RuntimeAwareSshReadState, environmentId: string): boolean {
return Boolean(state.runtimeStatusByEnvironmentId?.get(environmentId)?.status)
return isConnectedRuntimeHostState(
runtimeHostConnectionStateForEntry(state.runtimeStatusByEnvironmentId?.get(environmentId))
)
}
export function selectRuntimeAwareSshStatus(