Files
orca/src/shared/runtime-host-status.ts
T
Neil 96eb97aad6 fix(runtime): split the host-contact epoch out of the connection generation (#20359)
* fix(runtime): split the host-contact epoch out of the connection generation

`connectionGeneration` carried two meanings and one reader was always wrong.
holding the session mirror through an outage leaves its subscriptions stranded
and that edge was the only thing left to revive them. But the same value is the
mirror's cache key -- use-runtime-session-mirror-environment-key.ts keys the
subscription effect on it, every published frame is stamped with it, and
web-session-terminal-retirement-proof-ledger.ts drops retained proofs when it
moves. So the bump #20085 needed as a resubscribe signal re-keyed and rebuilt
the mirror after any brief flap, which is the #19647 symptom #19873/#20059 fix.

Measured first: with the reconnect bump deleted, an ended stream followed by
recovery issues zero resubscribes, and the mirror's subscribe call registers no
`onClose`, so main's terminal close is dropped. #20085's claim is true -- the
subscription really is dead after recovery -- so the trigger has to exist. It
just must not be the cache key.

Give each meaning its own value:

- `connectionGeneration` returns to identity only: a new runtime session, a
  re-pair, an explicit clear. A same-runtime return no longer moves it, so no
  stamp, fence or retained proof is invalidated by a flap.
- `hostContactEpoch` counts "the host answered again after we lost contact". It
  lives on the store entry and is read only as a dependency of the two
  subscription effects in use-web-session-tabs-sync.ts -- never passed to an
  installer, never part of `environmentKey`, so it cannot become a stamp.

`useRuntimeSessionMirrorEnvironmentKey` becomes
`useRuntimeSessionMirrorEnvironmentKeys`, returning `environmentKey` (identity)
and `resubscribeSignal` (the epoch edge) from the one target scan, so the hot
ownership scan is not doubled.

Each direction is pinned by its own test: removing the resubscribe dependency
fails only 'reinstalls both session-tabs subscriptions when the host answers
again'; restoring the reconnect bump fails only the two key-stability tests.

* test(runtime): pin the mirror hydration verdict across a host flap

The generation tests assert the key string; this asserts what the user feels.
The mirror's hydration verdict is stamped with the connection generation, so any
bump discards it and every mirrored pane re-parks -- the tab-list rebuild. Held
across an unverifiable probe, still discarded when the runtime id actually moved.

* test(runtime): build real host statuses instead of casting partials
2026-09-17 02:07:52 -07:00

96 lines
3.8 KiB
TypeScript

import type { RemoteRuntimeSharedConnectionDiagnostics } from './remote-runtime-shared-control-types'
import type { RuntimeRpcFailure, RuntimeRpcResponse } from './runtime-rpc-envelope'
import type { RuntimeStatus } from './runtime-types'
export const RUNTIME_HOST_STATUS_CHANNEL = 'runtimeEnvironments:statusChanged'
/** Local client state; never exchanged with the paired host. */
export type RuntimeHostStatusSnapshot = {
environmentId: string
pairingRevision: number
sequence: number
checkedAt: number
status: RuntimeStatus | null
verification: 'checking' | 'verified' | 'unavailable' | 'blocked'
transport: 'unknown' | 'connecting' | 'ready' | 'disconnected'
remoteControl?: RemoteRuntimeSharedConnectionDiagnostics | null
retired?: true
}
/**
* One client-side record of a host's last status probe: the projected `status`, and the
* `snapshot` evidence for what that projection is worth. Declared once rather than duck-typed
* per consumer — every field added here has been optional, so a local structural copy keeps
* typechecking against the store while silently missing whatever landed after it was written.
*/
export type RuntimeEnvironmentStatus = {
snapshot?: RuntimeHostStatusSnapshot
status: RuntimeStatus | null
remoteControl?: RuntimeStatus['remoteControl'] | null
appVersion?: string | null
checkedAt: number
/**
* Identity of the connection: which socket epoch retained state belongs to. Every cache key,
* stamp and settle fence compares this, so advancing it invalidates the session mirror.
*/
connectionGeneration?: number
/**
* Edge count of "the host answered again after we lost contact". A resubscribe trigger only —
* the streams died with the transport and nothing else revives them. Never an identity, a cache
* key, or a fence: that is `connectionGeneration`, and a flap must not move it (#19647).
*/
hostContactEpoch?: number
}
/**
* The last status the host actually answered with. The snapshot retains it across an
* unverifiable probe, so this survives a loss of contact; the entry's own `status` does not.
*/
export function lastVerifiedRuntimeStatus<Status = RuntimeStatus>(
entry: { status?: Status | null; snapshot?: { status: Status | null } | null } | null | undefined
): Status | null {
return entry?.snapshot?.status ?? entry?.status ?? null
}
/**
* The host's own verdict that this pairing is over: retired by an explicit disconnect, or
* refused — auth rejected or protocol mismatch, which stops every retry for good. Positive
* evidence, unlike a lost transport, so this is the only state that may withdraw a fact the
* host already gave us (docs/reference/ssh-execution-boundary.md).
*/
export function isRuntimeHostContactRevoked(
entry:
| { snapshot?: Pick<RuntimeHostStatusSnapshot, 'verification' | 'retired'> | null }
| null
| undefined
): boolean {
const snapshot = entry?.snapshot
return Boolean(snapshot && (snapshot.retired || snapshot.verification === 'blocked'))
}
export type RuntimeHostStatusResponse = RuntimeRpcResponse<RuntimeStatus>
export function runtimeHostStatusFailure(code: string, message: string): RuntimeRpcFailure {
return { id: 'status.get', ok: false, error: { code, message } }
}
export function runtimeHostStatusError(error: unknown): RuntimeRpcFailure {
const code =
error instanceof Error && 'code' in error && typeof error.code === 'string'
? error.code
: 'runtime_unavailable'
return runtimeHostStatusFailure(code, error instanceof Error ? error.message : String(error))
}
export function isRuntimeHostStatusBlocked(response: RuntimeRpcFailure): boolean {
return [
'unauthorized',
'forbidden',
'invalid_argument',
'invalid_runtime_response',
'protocol_version_mismatch',
'method_not_found',
'unsupported_method'
].includes(response.error.code)
}