mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
* feat(native-chat): enable Windows structured sessions
* fix(codex): prove native Windows process identity
* style(codex): format Windows session seam
* fix Windows structured Codex admission
* fix(windows): reprobe missing process identity capability
* fix(windows): decide folder-workspace WSL routing before the click
Review found pathUsesWslUnc exported but unused, and the folder composer
hardcoding worktreeUsesWslPath:false. Together those meant a folder picked
under a \\wsl.localhost\ parent routed to structured chat, then got refused
by the host and fell back AFTER the click -- which defeats the lane's own
design goal that create cannot fail after the click.
The group's parentPath is in scope at submit and the workspace is created
under it, so the parent decides WSL-ness pre-click. Wires pathUsesWslUnc
there and adds tests for the helper, including the unhydrated-store case
that previously threw.
* fix(windows): collapse the gate derivation to one call, restoring max-lines
CI static analysis failed: launch-agent-in-new-tab.ts crossed the 300-line
oxlint ceiling. Adding a max-lines disable is forbidden, so the two gate
derivations collapse into one readWindowsStructuredGateInputs() call --
a store-backed site now adds one line and one import name instead of two.
Better shape anyway: one derivation entry point rather than two reads a
call site must remember to pair.
* fix(windows): engage the legacy fallback when the host THROWS a refusal
Review found a P1 this merge composes: neither parent could reach it. At the
lane head the only structured entry was launch-agent-in-new-tab (full
store-backed WSL check); on main all win32 was refused. The merge enables
win32 in creation flows that pass no projectRuntime, so a WSL folder
workspace, a WSL-configured repo, or a repair-required runtime now routes
structured -- and the host refuses correctly, but by THROWING rather than
returning {ok:false, refusal}.
Callers engage their legacy-terminal fallback on the refusal CLASS, so an
unmapped throw arrives as a generic RPC rejection: no fallback, empty
workspace, error toast, prompt stranded in the launch outbox. Pre-merge the
same action opened a legacy terminal agent.
Map the host's thrown definitive refusals onto the refusal class at the
launch boundary, so every creation flow -- present and future -- degrades to
the legacy terminal instead of stranding. Narrow predicate: unrelated
failures (ECONNRESET, empty message, non-Error) still propagate untouched.
Ablation-proven: removing the mapping reddens the fallback test.
* fix(windows): teach the mobile RPC double the status probe the lane added
CI's first-ever run on this lane caught a pre-existing lane defect. The lane
changed status.get to resolve through
runtime.getStatusAfterWindowsProcessStartTimeProbe(), but never taught the
mobile-surface runtime double about it, so status.get failed for mobile
clients with "not a function". The lane's own test list did not include this
file and the lane had zero CI, so nothing ever ran it.
The real runtime always implements the method; the double omitted it.
* chore: merge current main and regenerate the localization runtime catalog
CI static analysis failed on a stale en-runtime-required.json: main added
onboarding integration-capability keys, and the generated catalog is checked
against the PR MERGE result, not the branch alone -- so it read clean locally
while failing in CI. Merging current main (90780acb85) and regenerating.
Gates after the merge: pnpm tc 0, oxlint 0, changed-code quality 0/56,
7 gate/lane test files 69 tests green.
* fix: route structured launches by execution host platform
* fix: recover paired structured session mirror on host swap
* Revert "fix: recover paired structured session mirror on host swap"
This reverts commit 81bfca0007.
* Revert "fix: route structured launches by execution host platform"
This reverts commit 47abbd354a.
* fix(windows): refuse structured chat in a paired web client
Reverts the two review-loop commits (restoring a tree byte-identical to the
validated head) and closes the hole they were aiming at, without their cost.
A paired web client's `platform` describes the browser's machine, not the host
that will run the agent, so the Windows gate cannot be evaluated there. Before
this, a browser on macOS driving a Windows runtime read "not win32", skipped the
creation-time proof entirely and allowed structured chat — fail-OPEN, the
dangerous direction, bypassing the guarantee this lane is built on.
`isWebClient` is a required input like the other gate fields, so the compiler
enumerated all seven call sites. Refusal is synchronous and fail-closed: no
async round-trip, no null window, no cache to invalidate — unlike keying on an
asynchronously-fetched host platform, which would have made every desktop
launch wait on a round-trip to fix a paired-web-only hole.
Paired web therefore gets the legacy chat until the host publishes eligibility
itself; that is the proper fix and belongs in its own PR.
Ablation-proven: removing the guard reddens both refusal tests; the
desktop-unaffected test is a preservation check and passes either way.
Gates: tc 0, oxlint 0.
Known open: repos-onboarding-folder-startup.test.ts fails on this branch and
passes on plain main — under investigation, NOT caused by this commit.
* test(onboarding): mock the web-client check the store path now reaches
The web-client refusal added `isWebClientLocation()` to the launch-route
inputs, which this suite's store path reaches while adding the FIRST folder.
The suite stubs `window` as `{ api }` with no `location`, so the function
cleared its `typeof window === 'undefined'` guard and then threw on
`window.location.pathname`.
That threw inside addNonGitFolder's own catch, so folder-1 never activated;
folder-2 then returned early (a project already existed) before reaching the
call at all, leaving exactly one activation with no startup seed.
Test artifact, not a product defect: a real renderer always has
`window.location`, so the seeding path is intact for users. Mocking the module
is the convention 7 other suites already use, and keeps product code free of
defensive branches that only exist to satisfy a stub.
Ablation-proven: removing the mock reproduces the original failure exactly.
* fix(renderer): make the web-client check total over a partial window
isWebClientLocation() guarded `typeof window === 'undefined'` and then assumed
`window.location` existed. A window stubbed without a location cleared the
guard and threw on `.pathname`.
That matters because this branch put the call on the launch-routing path,
where the throw is swallowed by the caller's catch and silently becomes a
FAILED LAUNCH rather than a visible error. CI caught it as 9 failures in
launch-work-item-direct.test.ts.
I previously "fixed" this by mocking the module in the one suite I knew about.
That was whack-a-mole against an unbounded set, and it missed this one. The
defect is the partial-window assumption, so fix it there: the mock is removed
from the onboarding suite and both suites now pass on the hardening alone.
Ablation-proven: reverting to the unguarded form reddens 11 tests across the
new unit suite and launch-work-item-direct.
Gates: tc 0, oxlint 0, changed-code quality 0/58.
* Move Codex's Windows structured-chat eligibility onto the host createSupport probe
The renderer no longer decides Codex win32 eligibility: launchStructuredAgentSession
probes agentSession.createSupport for both providers, the host answers via
supportsCodexStructuredLocation (process start-time proof + WSL refusal), and the
create path re-checks live. Deletes the client-side windows gate module and its
routing inputs (windowsProcessStartTime, worktreeUsesWslPath, isWebClient, platform)
from six call sites. Splits killCodexAppServerProcessTree out of
codex-app-server-session to hold the max-lines ceiling without a disable.
* fix(ci): keep pnpm lockfile stable
* test(windows): align foreground snapshot flags
* Restore main's pane-snapshot flag contract
Main asks for CreationTime on both projections; this branch's hot-path
isolation went away with the async probe it served.
---------
Co-authored-by: Orca Worker <orca-worker@localhost>
Co-authored-by: Merge Sim <sim@local>
Co-authored-by: Merge Sim <merge@localhost>
280 lines
9.7 KiB
TypeScript
280 lines
9.7 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'
|
|
|
|
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 prove process creation times for PID ownership. */
|
|
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
|
|
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'
|
|
}
|
|
}
|
|
|
|
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[]
|
|
}
|
|
|
|
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.
|
|
*
|
|
* Paired with `snapshotVersion: 0` 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'
|
|
|
|
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
|
|
}
|
|
|
|
export type RuntimeMobileSessionCreateTerminalResult = {
|
|
tab: RuntimeMobileSessionTerminalClientTab
|
|
publicationEpoch: string
|
|
snapshotVersion: number
|
|
}
|
|
|
|
export type RuntimeMobileSessionTabsRemovedResult = RuntimeMobileSessionTabsResult & {
|
|
removed: true
|
|
activeGroupId: null
|
|
activeTabId: null
|
|
activeTabType: null
|
|
tabs: []
|
|
}
|