Files
orca/src/shared/runtime-session-contracts.ts
T
Jinwoo HongandClaude 24bda717f4 fix(terminal): a slow window no longer makes orca terminal create fail and then duplicate (#28158)
* fix(terminal): start CLI-created workspace terminals in main so a slow window cannot duplicate them

A focused or interactive-agent `orca terminal create` asked the window to make the
terminal and reported failure after 10 s, while the window still made it later; a
retry then made a second one. Main now starts every terminal that names a workspace
and shows it in the window best-effort, as it already did for every other create.

* fix(terminal): start a main-created terminal at its worktree's measured pane size

An unfocused CLI agent opens as an inactive tab that is not resized until the user
opens it, so starting it at 120x40 left its output wrapped to 120 columns. Start it at
the size of the pane active in the worktree's active group, where the new tab opens;
fall back to 120x40 only when that worktree has no measured pane.

* fix(cli): keep sending rendererBacked for hosts before v1.4.211

Hosts without #22384 leave a background-started agent's startup queries unanswered,
so a new CLI talking to an old, still-running host must keep routing interactive agents
to the window. Newer hosts ignore the flag.

* fix(terminal): start host-created terminals at the measured size of their tab group

The window publishes, on its graph sync to main only, the grid an unsplit terminal
would get in each shown tab group, computed by one pure function that mirrors
FitAddon.proposeDimensions and is pinned against it; a ResizeObserver on each group
body republishes when that grid changes. Main starts a new terminal at the size of the
group it opens in (the named group, else the active one) and falls back to 120x40 only
when no window measured it. This replaces the active-pane lookup, which mis-sized split
groups and could read a phone's fitted size.

* fix(terminal): measure group sizes from an untitled default-font pane and use the live active group

A pane with a title or banner pushes its xterm host down, which skewed every group's
row count; skip it like a zoomed pane. Size from the window's latest published active
group, which is where its reveal places the tab, before the saved session's.

* fix(terminal): size a CLI agent in a workspace the window has not shown

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(terminal): learn the single-group inset only from bodies shown in the content area

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: rerun PR checks against main with #28297

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: rerun PR checks against main with #28301

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-13 15:08:45 -04:00

306 lines
11 KiB
TypeScript

import type { AgentStatusOrchestrationContext } from './agent-status-types'
import type { RemoteServerUpdateSupport } from './remote-server-update'
import type { RemoteRuntimeSharedConnectionDiagnostics } from './remote-runtime-shared-control-types'
import type { RuntimeHostConnectionState } from './runtime-host-connection-state'
import type { RuntimeCapability } from './protocol-version'
import type {
RuntimeBrowserUnavailableReason,
RuntimeDegradation
} from './runtime-capability-degradation'
import type { TabGroupLayoutNode } from './tab-types'
import type { TerminalPaneLayoutNode } from './terminal-tab-types'
import type {
RuntimeMobileSessionClientTab,
RuntimeMobileSessionSnapshotTab,
RuntimeMobileSessionTerminalClientTab
} from './runtime-mobile-session-tab-contracts'
import type { CliStatusCaller } from './orchestration-caller-status'
import type { RuntimeHostDescriptor } from './runtime-host-descriptor'
export type * from './runtime-mobile-session-tab-contracts'
export type RuntimeGraphStatus = 'ready' | 'reloading' | 'unavailable'
export type RuntimeDesktopWindowStatus = 'available' | 'openable' | 'initializing' | 'blocked'
export const HEADLESS_RUNTIME_WINDOW_ID = 0
export type DeviceScope = 'mobile' | 'runtime'
export type RuntimeTerminalDriverState =
| { kind: 'idle' }
| { kind: 'desktop' }
| { kind: 'mobile'; clientId: string }
export type RuntimeBrowserDriverState = RuntimeTerminalDriverState
export const BROWSER_UNAVAILABLE_ERROR_CODE = 'browser_unavailable' as const
// Why: one sentence per cause, each naming the thing the operator can change. The host
// renders these so an older client still shows an accurate reason it cannot decode.
const BROWSER_UNAVAILABLE_MESSAGES: Record<RuntimeBrowserUnavailableReason, string> = {
unconfigured:
'Browser automation has no backend on this host. Install the Orca desktop app, or set ORCA_BROWSER_EXECUTABLE to a Chromium executable.',
driver_missing:
'ORCA_BROWSER_EXECUTABLE is set, but the bundled agent-browser driver is missing or not executable on this host, so Chromium cannot be driven.',
executable_not_found: 'ORCA_BROWSER_EXECUTABLE points at a path that does not exist.',
executable_not_executable:
'ORCA_BROWSER_EXECUTABLE points at a file that is not executable by this host.',
electron_start_failed: 'The installed Electron browser provider failed to start.',
chromium_start_failed:
'The Chromium browser provider named by ORCA_BROWSER_EXECUTABLE failed to start.',
provider_unhealthy: 'The browser provider started but is no longer answering health checks.',
desktop_window_unavailable:
'Browser automation on this host needs a desktop window, and none is available.',
unknown: 'Browser automation is unavailable on this host, and the cause could not be determined.'
}
export function browserUnavailableMessage(
reason: RuntimeBrowserUnavailableReason,
detail?: string
): string {
const base = BROWSER_UNAVAILABLE_MESSAGES[reason]
return detail ? `${base} (${detail})` : base
}
export type RuntimeStatus = {
runtimeId: string
/** Authenticated requester identity. Missing for in-process callers and older hosts. */
pairedDeviceId?: string
rendererGraphEpoch: number
graphStatus: RuntimeGraphStatus
authoritativeWindowId: number | null
desktopWindowStatus?: RuntimeDesktopWindowStatus
liveTabCount: number
liveLeafCount: number
runtimeProtocolVersion?: number
minCompatibleRuntimeClientVersion?: number
capabilities?: RuntimeCapability[]
/** Optional policy for clients that negotiated worktree.create-idempotency.v1. */
worktreeCreateIdempotency?: {
dedupeTtlMs: number
}
/** True only when this Windows host can read process creation times. TEMPORARY: read only by
* older clients, which keep re-probing WSL until it is true; remove after their window. */
windowsProcessStartTimeAvailable?: boolean
/**
* Optional for mixed-version peers. Absence means the host predates structured
* degradation reporting, not that the host proved every optional feature available.
*/
degradations?: RuntimeDegradation[]
appVersion?: string
remoteUpdateSupport?: RemoteServerUpdateSupport
remoteControl?: RemoteRuntimeSharedConnectionDiagnostics | null
hostPlatform?: NodeJS.Platform
/** Optional display name reported by the answering runtime. */
machineName?: string
/** Absent from older hosts and from hosts whose profile could not persist an installation id. */
hostDescriptor?: RuntimeHostDescriptor
terminalWindowsShell?: string | null
deviceScope?: DeviceScope
floatingWorkspaceEnabled?: boolean
// COMPAT(runtimeStatusMobileAliases): added 2026-05-15 for older mobile builds.
protocolVersion?: number
minCompatibleMobileVersion?: number
}
export type CliRuntimeState =
| 'not_running'
| 'starting'
| 'ready'
| 'graph_not_ready'
| 'stale_bootstrap'
export type CliStatusResult = {
target?: { kind: 'local' } | { kind: 'environment'; environment: string }
app: {
running: boolean
pid: number | null
desktopWindowStatus?: RuntimeDesktopWindowStatus
}
runtime: {
state: CliRuntimeState
reachable: boolean
/** Canonical runtime transport verdict, when the caller has runtime evidence. */
connectionState?: RuntimeHostConnectionState
runtimeId: string | null
appVersion?: string
remoteUpdateSupport?: RemoteServerUpdateSupport
capabilities?: RuntimeCapability[]
degradations?: RuntimeDegradation[]
}
graph: {
state: RuntimeGraphStatus | 'not_running' | 'starting'
}
/** This process's Orca session ID when it runs as an Orca session; see `CliStatusCaller`. */
caller?: CliStatusCaller
}
export type RuntimeSyncedTab = {
tabId: string
worktreeId: string
title: string | null
activeLeafId: string | null
layout: TerminalPaneLayoutNode | null
}
export type RuntimeSyncedLeaf = {
tabId: string
worktreeId: string
leafId: string
paneRuntimeId: number
ptyId: string | null
paneTitle?: string | null
title?: string | null
/** True when this leaf is retained by a parked PTY watcher, not mounted in the renderer. */
parked?: boolean
}
export type RuntimeSyncWindowGraph = {
tabs: RuntimeSyncedTab[]
leaves: RuntimeSyncedLeaf[]
mobileSessionTabs?: RuntimeMobileSessionTabsSnapshot[]
unchangedMobileSessionWorktrees?: string[]
/** Terminal grid that fills each shown tab group, by group id, so main starts a new tab's
* terminal at its group's size. Window-to-main only: never copied into session tabs. */
terminalGroupGridSizes?: Record<string, { cols: number; rows: number }>
/** The grid a workspace with a single tab group gets, for one the window is not showing. */
singleGroupWorkspaceGridSize?: { cols: number; rows: number }
}
export type RuntimeRendererSyncWindowGraph = RuntimeSyncWindowGraph & {
rendererGeneration: string
}
export type RuntimeNativeChatLaunchDraftResolution = {
tabId: string
text: string
createdAt: number
}
export type RuntimeSyncWindowGraphResult = RuntimeStatus & {
agentOrchestrationByPaneKey?: Record<string, AgentStatusOrchestrationContext>
nativeChatLaunchDraftResolutions?: RuntimeNativeChatLaunchDraftResolution[]
mobileSessionResyncWorktrees?: string[]
}
export type RuntimeMobileSessionTabGroup = {
id: string
activeTabId: string | null
tabOrder: string[]
recentTabIds?: string[]
}
type RuntimeMobileSessionTabMoveBase = {
tabId: string
targetGroupId: string
}
export type RuntimeMobileSessionTabMove =
| (RuntimeMobileSessionTabMoveBase & { kind: 'reorder'; tabOrder: string[] })
| (RuntimeMobileSessionTabMoveBase & { kind: 'move-to-group'; index?: number })
| (RuntimeMobileSessionTabMoveBase & {
kind: 'split'
splitDirection: 'left' | 'right' | 'up' | 'down'
})
export type RuntimeMobileSessionTabMoveResult = { moved: true }
export type RuntimeMobileSessionTabCloseResult = {
closed: true
refused?: true
refusalReason?:
| 'missing-intent'
| 'stale-publication'
| 'stale-terminal'
| 'live-host-pty'
| 'unknown-liveness'
| 'retirement-owner'
snapshotRepublished?: true
}
export type RuntimeSessionTabCloseReason = 'user' | 'pty-exit' | 'cleanup'
/**
* The publication epoch a runtime answers with for a worktree it has published nothing for yet —
* the state every worktree is in for a moment after the host process restarts.
*
* Bare or with a paired client's navigation suffix, it marks a synthesized placeholder, not a host answer: the
* runtime is saying "ask me later", not "those tabs are gone". Clients must not read absence from
* such a frame as evidence a tab was closed.
*/
export const UNPUBLISHED_WORKTREE_PUBLICATION_EPOCH = 'none'
/** Suffix a host appends to the epoch when projecting a snapshot for one paired client's navigation. */
export const CLIENT_NAVIGATION_PUBLICATION_EPOCH_SUFFIX = ':client-navigation'
export type RuntimeMobileSessionTabsSnapshot = {
worktree: string
/** Immutable catalog identity used to fence snapshots across path reuse. */
worktreeInstanceId?: string
publicationEpoch: string
snapshotVersion: number
activeGroupId: string | null
activeTabId: string | null
activeTabType: 'terminal' | 'markdown' | 'file' | 'browser' | 'agent-session' | null
tabGroups?: RuntimeMobileSessionTabGroup[]
tabGroupLayout?: TabGroupLayoutNode | null
retiredTerminalSurfaces?: RuntimeMobileSessionRetiredTerminalSurface[]
tabs: RuntimeMobileSessionSnapshotTab[]
}
export type RuntimeMobileSessionRetiredTerminalSurface = {
parentTabId: string
leafId: string
ptyId: string
terminal: string
incarnationId?: string
}
export type RuntimeMobileSessionTabsResult = {
worktree: string
publicationEpoch: string
snapshotVersion: number
navigationIntent?: 'follow'
activeGroupId: string | null
activeTabId: string | null
activeTabType: 'terminal' | 'markdown' | 'file' | 'browser' | 'agent-session' | null
tabGroups?: RuntimeMobileSessionTabGroup[]
tabGroupLayout?: TabGroupLayoutNode | null
retiredTerminalSurfaces?: RuntimeMobileSessionRetiredTerminalSurface[]
tabs: RuntimeMobileSessionClientTab[]
/**
* Set while a freshly started runtime has not yet taken back the client-hosted pages its paired
* hosts are still holding. Such a snapshot is authoritative about terminals, which it rehydrated
* from disk, but silently empty of browser rows it has simply not heard about yet — so a client
* must not read the absence of its own client-hosted rows here as "the host closed them".
*
* Always bounded: the runtime clears it once a host attaches, and drops it on a deadline so a
* host that never returns cannot hold rows open forever.
*/
clientHostedPagesUnreconciled?: true
/**
* Set while this runtime has no structured-chat host that can say which chats exist because the
* chat journal will not open. The snapshot is still authoritative about everything else, but its
* missing `agent-session` rows mean "cannot tell", not "closed".
*
* Cleared by the first tab restore a host answers. Not bounded by a deadline: the chats are
* durable on disk, so holding their tabs strands nothing.
*/
agentSessionsUnverifiable?: true
}
export type RuntimeMobileSessionCreateTerminalResult = {
tab: RuntimeMobileSessionTerminalClientTab
publicationEpoch: string
snapshotVersion: number
}
export type RuntimeMobileSessionTabsRemovedResult = RuntimeMobileSessionTabsResult & {
removed: true
activeGroupId: null
activeTabId: null
activeTabType: null
tabs: []
}