mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* 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
96 lines
3.8 KiB
TypeScript
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)
|
|
}
|