mirror of
https://github.com/stablyai/orca.git
synced 2026-09-28 00:02:41 +00:00
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.
This commit is contained in:
@@ -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,90 @@
|
||||
// @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 { useAppStore } from '../store'
|
||||
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 snapshot(overrides: Partial<RuntimeHostStatusSnapshot> = {}): RuntimeHostStatusSnapshot {
|
||||
return {
|
||||
environmentId: ENVIRONMENT_ID,
|
||||
pairingRevision: 1,
|
||||
sequence: 1,
|
||||
checkedAt: 1,
|
||||
status: { runtimeId: 'runtime-a' } as RuntimeHostStatusSnapshot['status'],
|
||||
verification: 'verified',
|
||||
transport: 'ready',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function setStatusEntry(entry: unknown): void {
|
||||
useAppStore.setState({
|
||||
runtimeStatusByEnvironmentId: new Map([
|
||||
[ENVIRONMENT_ID, entry]
|
||||
]) as AppState['runtimeStatusByEnvironmentId']
|
||||
})
|
||||
}
|
||||
|
||||
describe('landing preflight under an unverifiable host probe', () => {
|
||||
beforeEach(() => {
|
||||
invalidate.mockClear()
|
||||
refresh.mockClear()
|
||||
useAppStore.setState(
|
||||
{
|
||||
...initialState,
|
||||
settings: { activeRuntimeEnvironmentId: ENVIRONMENT_ID } as AppState['settings'],
|
||||
invalidatePreflightStatus: invalidate,
|
||||
refreshPreflightStatus: refresh
|
||||
},
|
||||
true
|
||||
)
|
||||
setStatusEntry({ status: { runtimeId: 'runtime-a' }, 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,140 @@
|
||||
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 = { runtimeId: 'runtime-a' } as RuntimeStatus
|
||||
|
||||
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 {
|
||||
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
|
||||
}) {
|
||||
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,178 @@
|
||||
// @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 { 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 { AppState } from '@/store/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 verifiedSnapshot(): RuntimeHostStatusSnapshot {
|
||||
return {
|
||||
environmentId: ENV_A,
|
||||
pairingRevision: REVISION_A,
|
||||
sequence: 1,
|
||||
checkedAt: 1,
|
||||
status: { runtimeId: 'runtime-a' } as RuntimeHostStatusSnapshot['status'],
|
||||
verification: 'verified',
|
||||
transport: 'ready'
|
||||
}
|
||||
}
|
||||
|
||||
function setRuntimeStatusEntry(entry: unknown): void {
|
||||
useAppStore.setState({
|
||||
runtimeStatusByEnvironmentId: new Map([
|
||||
[ENV_A, entry]
|
||||
]) as AppState['runtimeStatusByEnvironmentId']
|
||||
})
|
||||
}
|
||||
|
||||
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()
|
||||
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: { runtimeId: 'runtime-a' },
|
||||
snapshot: verifiedSnapshot(),
|
||||
connectionGeneration: 1
|
||||
}
|
||||
]
|
||||
]) as AppState['runtimeStatusByEnvironmentId']
|
||||
},
|
||||
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' },
|
||||
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: { runtimeId: 'runtime-b' },
|
||||
snapshot: {
|
||||
...verifiedSnapshot(),
|
||||
sequence: 2,
|
||||
checkedAt: 2,
|
||||
status: { runtimeId: 'runtime-b' } as RuntimeHostStatusSnapshot['status']
|
||||
},
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user