fix(wsl): hold main's background WSL start before the cwd probe; let the user's runtime start check a WSL folder (#35971)

* fix(wsl): hold main's background WSL start in the shared preflight; folder launch scope honours the user's act

Main's background start (no wslUserAct) is now held in resolvePtySpawnStartupTarget, before the
startup cwd probe and any provider. A runtime start the user asked for (CLI, window, phone, split)
checks a WSL folder workspace's path as their act, so it may start the distro; background work is
refused with the stopped-distro message instead of folder_workspace_path_unavailable.

* fix(wsl): agent-session create and user resume check a WSL folder as the user's act; background holds with Start

* fix(wsl): agent-session ensure and a failed start's kept pane pass the user's act to the WSL folder check

* test(e2e): a prepared terminal.create dedupes as the caller's act
This commit is contained in:
Neil
2026-10-29 13:31:17 -07:00
committed by GitHub
parent 29a6ff3ffa
commit e8e0803672
27 changed files with 447 additions and 72 deletions
+2 -1
View File
@@ -94,7 +94,8 @@ export async function preparePtyIpcSpawnPreflight(ctx: PtyIpcSpawnState): Promis
resolveStartupCwd: ctx.deps.resolvePtySpawnStartupCwd,
...(args.wslPassiveRestore === true
? {
passiveWslRestore: {
wslStartHold: {
wslPassiveRestore: true,
sessionId: ctx.effectiveSessionId,
isNewSession: ctx.ownsNewIncarnation,
attachOnly: args.attachOnlyView === true
@@ -52,6 +52,11 @@ vi.mock('../../codex/codex-pane-account-registry', () =>
vi.mock('../../codex/codex-state-db-backfill-recovery', () =>
import('./pty-ipc-mock-registry').then((m) => m.codexBackfillRecoveryModuleMock())
)
// Fake-win32 runs have no wsl.exe: the distro runs, so main's background-start hold passes (E9).
vi.mock('../../wsl-background-distro-gate', async (importOriginal) => ({
...(await importOriginal<object>()),
readWslDistroRunningState: vi.fn(async () => 'running')
}))
describe('registerPtyHandlers', () => {
const { handlers, mainWindow } = setupPtyIpcSuite()
@@ -130,6 +130,11 @@ vi.mock('../../codex/codex-state-db-backfill-recovery', () =>
vi.mock('../../../shared/openssh-default-shell', () => ({
readOpenSshDefaultShell: vi.fn(() => '')
}))
// Fake-win32 runs have no wsl.exe: the distro runs, so main's background-start hold passes (E9).
vi.mock('../../wsl-background-distro-gate', async (importOriginal) => ({
...(await importOriginal<object>()),
readWslDistroRunningState: vi.fn(async () => 'running')
}))
// Exercise real window-lane spawning through provider, telemetry and pane binding.
describe('window lane: pty:spawn request -> provider', () => {
@@ -3,13 +3,12 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { BrowserWindow } from 'electron'
import { getDefaultSettings } from '../../../../shared/constants'
import { finishPtyShutdown } from '../provider/liveness'
import { prepareRuntimePtySpawn } from './spawn-preflight'
import { buildRuntimePtySpawnOptions } from './spawn-options'
import { createRuntimePtySpawnState, type RuntimePtySpawnArgs } from './spawn-state'
import type { PtyRuntimeControllerDeps } from './controller-deps'
import { makeRuntimePtySpawnTestDeps } from './spawn-preflight-test-deps-fixture'
import { ClaudeProfileRouter } from '../../../claude-accounts/claude-profile-router'
import { installClaudeProfileRouter } from '../../../claude-accounts/claude-profile-installed-router'
@@ -42,41 +41,7 @@ const HOST_DEFAULT_SHELL = 'powershell.exe'
const hostPlatform = process.platform
function makeDeps(): PtyRuntimeControllerDeps {
const noCodexResumeLaunch: PtyRuntimeControllerDeps['noCodexResumeLaunch'] = (command) => ({
codexResumeHome: null,
command,
notifyResumeUnavailable: false,
droppedResumeArgv: false,
providerSession: null
})
return {
store: undefined,
getSettings: () => ({
...getDefaultSettings('/tmp'),
terminalWindowsShell: HOST_DEFAULT_SHELL
}),
adoptStablePane: async () => null,
getLocalPtyStartupPromise: () => undefined,
getLocalPtyProviderStartupPromise: () => undefined,
prepareCodexResumeHome: () => null,
resolveCodexResumeLaunch: async (command) => noCodexResumeLaunch(command),
noCodexResumeLaunch,
reconcileSharedRuntimeResumeHome: async (resumeHome) => resumeHome.codexHomePath,
stripSequencedStartupResumeArgv: (env) => env,
assertWorkspaceAcceptsNewPty: () => undefined,
resolvePtySpawnStartupCwd: (_worktreeId, cwd) => cwd,
requestSerializedBuffer: async () => null,
shutdownProviderAndDetectExit: async () => false,
rememberSyntheticKillExit: () => {},
rememberRetiredRejectedPty: () => {},
sendPtyExitToRenderer: () => {},
sendPtySpawnedToRenderer: () => {},
finishPtyShutdown,
trustedTerminalHandleEnv: new Set(),
retiredRejectedPtyIds: new Map(),
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only `operations.ts` (write/clearBuffer) reads `mainWindow`; the spawn preflight and option build never touch it, and a real BrowserWindow cannot exist in vitest.
mainWindow: {} as BrowserWindow
}
return makeRuntimePtySpawnTestDeps(HOST_DEFAULT_SHELL)
}
async function resolveSpawnOptions(
@@ -216,7 +181,9 @@ describe('runtime pty spawn preflight: Claude account routing in a WSL pane', ()
rows: 40,
cwd: '\\\\wsl.localhost\\Ubuntu\\home\\u',
shellOverride: 'wsl.exe',
env: { KEEP: '1' }
env: { KEEP: '1' },
// The user's start: a background one holds until the distro is proven running (E9).
wslUserAct: true
}
const ctx = createRuntimePtySpawnState(makeDeps(), args)
await expect(prepareRuntimePtySpawn(ctx)).resolves.toBeNull()
@@ -0,0 +1,45 @@
import type { BrowserWindow } from 'electron'
import { getDefaultSettings } from '../../../../shared/constants'
import { finishPtyShutdown } from '../provider/liveness'
import type { PtyRuntimeControllerDeps } from './controller-deps'
/** Runtime spawn deps for preflight tests: no store, a host default shell, and inert side effects. */
export function makeRuntimePtySpawnTestDeps(
hostDefaultShell = 'powershell.exe'
): PtyRuntimeControllerDeps {
const noCodexResumeLaunch: PtyRuntimeControllerDeps['noCodexResumeLaunch'] = (command) => ({
codexResumeHome: null,
command,
notifyResumeUnavailable: false,
droppedResumeArgv: false,
providerSession: null
})
return {
store: undefined,
getSettings: () => ({
...getDefaultSettings('/tmp'),
terminalWindowsShell: hostDefaultShell
}),
adoptStablePane: async () => null,
getLocalPtyStartupPromise: () => undefined,
getLocalPtyProviderStartupPromise: () => undefined,
prepareCodexResumeHome: () => null,
resolveCodexResumeLaunch: async (command) => noCodexResumeLaunch(command),
noCodexResumeLaunch,
reconcileSharedRuntimeResumeHome: async (resumeHome) => resumeHome.codexHomePath,
stripSequencedStartupResumeArgv: (env) => env,
assertWorkspaceAcceptsNewPty: () => undefined,
resolvePtySpawnStartupCwd: (_worktreeId, cwd) => cwd,
requestSerializedBuffer: async () => null,
shutdownProviderAndDetectExit: async () => false,
rememberSyntheticKillExit: () => {},
rememberRetiredRejectedPty: () => {},
sendPtyExitToRenderer: () => {},
sendPtySpawnedToRenderer: () => {},
finishPtyShutdown,
trustedTerminalHandleEnv: new Set(),
retiredRejectedPtyIds: new Map(),
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only `operations.ts` (write/clearBuffer) reads `mainWindow`; the spawn preflight and option build never touch it, and a real BrowserWindow cannot exist in vitest.
mainWindow: {} as BrowserWindow
}
}
@@ -0,0 +1,63 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as BackgroundDistroGateModule from '../../../wsl-background-distro-gate'
import { wslDistroHoldMessage } from '../../../../shared/wsl-distro-stopped-hold'
import { prepareRuntimePtySpawn } from './spawn-preflight'
import { createRuntimePtySpawnState, type RuntimePtySpawnArgs } from './spawn-state'
import { makeRuntimePtySpawnTestDeps } from './spawn-preflight-test-deps-fixture'
const mocks = vi.hoisted(() => ({
readWslDistroRunningState: vi.fn<() => Promise<'running' | 'stopped' | 'unverifiable'>>()
}))
vi.mock('../../../wsl-background-distro-gate', async (importOriginal) => ({
...(await importOriginal<typeof BackgroundDistroGateModule>()),
readWslDistroRunningState: mocks.readWslDistroRunningState
}))
const WSL_CWD = '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo'
const hostPlatform = process.platform
async function prepare(args: Partial<RuntimePtySpawnArgs>): Promise<void> {
const ctx = createRuntimePtySpawnState(makeRuntimePtySpawnTestDeps(), {
cols: 120,
rows: 40,
cwd: WSL_CWD,
...args
})
try {
await prepareRuntimePtySpawn(ctx)
} finally {
ctx.finishTerminalInstall()
}
}
beforeEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
mocks.readWslDistroRunningState.mockReset()
mocks.readWslDistroRunningState.mockResolvedValue('stopped')
})
afterEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: hostPlatform })
})
describe("main's background start in a stopped WSL distro (E9)", () => {
it('holds in the shared preflight, whichever provider would run it', async () => {
await expect(prepare({})).rejects.toThrow(wslDistroHoldMessage('Ubuntu', 'stopped'))
expect(mocks.readWslDistroRunningState).toHaveBeenCalledOnce()
})
it('names an unanswered read as unverifiable', async () => {
mocks.readWslDistroRunningState.mockResolvedValue('unverifiable')
await expect(prepare({})).rejects.toThrow(wslDistroHoldMessage('Ubuntu', 'unverifiable'))
})
it("never holds the user's start", async () => {
await expect(prepare({ wslUserAct: true })).resolves.toBeUndefined()
expect(mocks.readWslDistroRunningState).not.toHaveBeenCalled()
})
it('starts once the distro runs', async () => {
mocks.readWslDistroRunningState.mockResolvedValue('running')
await expect(prepare({})).resolves.toBeUndefined()
})
})
+5 -1
View File
@@ -76,7 +76,11 @@ export async function prepareRuntimePtySpawn(
projectRuntimeSource: { kind: 'store', store: ctx.deps.store },
preAdopted: ctx.preAdoptedStablePane !== null,
getSettings: ctx.deps.getSettings,
resolveStartupCwd: ctx.deps.resolvePtySpawnStartupCwd
resolveStartupCwd: ctx.deps.resolvePtySpawnStartupCwd,
// Default-deny, as spawn-options' `wslBackgroundStart`: held before the cwd probe and any provider.
...(args.wslUserAct === true
? {}
: { wslStartHold: { wslBackgroundStart: true, sessionId: ctx.sessionId } })
})
ctx.cwd = target.cwd
ctx.requestedCwd = target.requestedCwd
+7 -4
View File
@@ -55,8 +55,11 @@ type PtySpawnStartupArgs = {
cwd: string | undefined,
missingDirFallback?: TerminalStartupCwdMissingDirFallback
) => string | undefined
/** A restore the user did not ask for; held here, before any provider or cwd probe can boot WSL. */
passiveWslRestore?: Pick<PtySpawnOptions, 'sessionId' | 'isNewSession' | 'attachOnly'>
/** A restore or background start the user did not ask for; held here, before any provider or cwd probe can boot WSL. */
wslStartHold?: Pick<
PtySpawnOptions,
'wslPassiveRestore' | 'wslBackgroundStart' | 'sessionId' | 'isNewSession' | 'attachOnly'
>
}
export async function resolvePtySpawnStartupTarget(
@@ -73,10 +76,10 @@ export async function resolvePtySpawnStartupTarget(
args.shellOverride,
process.env.COMSPEC || 'powershell.exe'
)
if (args.passiveWslRestore && !args.preAdopted) {
if (args.wslStartHold && !args.preAdopted) {
// E9: whichever provider runs (daemon, degraded or off), and before the cwd probe's `wsl.exe`.
await holdPassiveRestoreOfStoppedWslDistro(
{ ...args.passiveWslRestore, wslPassiveRestore: true },
args.wslStartHold,
resolveWslSessionContext({
cwd: args.cwd,
sessionId: args.sessionId,
@@ -109,7 +109,14 @@ export abstract class OrcaRuntimeWithCreateAgentSession extends OrcaRuntimeWithG
const reclaim: AgentSessionCreateOperation['reclaim'] = {}
const operation = (async (): Promise<RuntimeCreateAgentSessionResult> => {
// Reserve before async preflight so racing retries cannot independently dispatch.
const workspace = await this.resolveTerminalWorkspaceLaunchScope(request.worktree)
// Only the agent-session RPC reaches here: the caller's own act, as its spawn below.
const workspace = await this.resolveTerminalWorkspaceLaunchScope(
request.worktree,
undefined,
{
wslUserAct: true
}
)
if (
!(await this.executionOwnerSupportsAgentSessionOperation(
workspace,
@@ -228,7 +228,8 @@ describe('agent session RPC methods', () => {
'id:worktree-1',
undefined,
false,
expect.any(Function)
expect.any(Function),
{ wslUserAct: true }
)
expect(createTerminal).toHaveBeenCalledWith('id:worktree-1', {
wslUserAct: true,
@@ -46,7 +46,8 @@ describe('terminal.create RPC idempotency', () => {
'id:worktree-1',
'mutation-1',
false,
expect.any(Function)
expect.any(Function),
{ wslUserAct: true }
)
expect(createTerminal).toHaveBeenCalledWith(
'id:worktree-1',
@@ -118,7 +118,8 @@ export const TERMINAL_LIFECYCLE_METHODS = [
leafId: params.leafId,
...(params.spawnIntent ? { spawnIntent: params.spawnIntent } : {}),
...(preAllocatedHandle ? { preAllocatedHandle } : {})
})
}),
{ wslUserAct: true }
)
// Switch point. A CLI start is work in the workspace; a window or phone tab is a person opening one.
if (
@@ -0,0 +1,76 @@
import type * as FsPromises from 'node:fs/promises'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const statMock = vi.hoisted(() => vi.fn<(path: string) => Promise<{ isDirectory(): boolean }>>())
vi.mock('node:fs/promises', async (importOriginal) => ({
...(await importOriginal<typeof FsPromises>()),
stat: statMock
}))
vi.mock('../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
// Fragments stay side-effect ordered: mocks, then lifecycle, then fixtures.
const { OrcaRuntimeService } = await import('./orca-runtime-test-mocks.spec')
await import('./orca-runtime-test-lifecycle.spec')
const {
createFolderWorkspaceRuntimeStore,
makeFolderProjectGroup,
makeFolderWorkspace,
TEST_FOLDER_WORKSPACE_KEY
} = await import('./orca-runtime-test-fixtures.spec')
const { blockedWslDistroForPath } = await import('../wsl/wsl-unc-access-gate')
const { isWslUserActFor } = await import('../wsl/wsl-user-act-scope')
const PARENT = '\\\\wsl.localhost\\Ubuntu\\home\\me'
const FOLDER = `${PARENT}\\notes`
const hostPlatform = process.platform
const statsInUserAct: boolean[] = []
function failedStart(wslUserAct: boolean): Promise<unknown> {
const runtime = new OrcaRuntimeService(
createFolderWorkspaceRuntimeStore(
makeFolderWorkspace({ folderPath: FOLDER }),
makeFolderProjectGroup({ parentPath: PARENT })
)
)
return runtime
.createTerminal(`id:${TEST_FOLDER_WORKSPACE_KEY}`, {
keepPaneOnStartFailure: true,
startFailure: new Error('agent runtime needs repair'),
...(wslUserAct ? { wslUserAct: true as const } : {})
})
.catch(() => undefined)
}
beforeEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
vi.spyOn(console, 'warn').mockImplementation(() => {})
statsInUserAct.length = 0
statMock.mockReset()
statMock.mockImplementation(async () => {
statsInUserAct.push(isWslUserActFor('Ubuntu'))
return { isDirectory: () => true }
})
// Stopped, as the real gate reads it: only the user's act on the distro passes.
vi.mocked(blockedWslDistroForPath).mockImplementation(async () =>
isWslUserActFor('Ubuntu') ? null : { distro: 'Ubuntu', state: 'stopped' }
)
})
afterEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: hostPlatform })
vi.mocked(blockedWslDistroForPath).mockReset()
vi.mocked(blockedWslDistroForPath).mockResolvedValue(null)
vi.restoreAllMocks()
})
describe('the pane a failed start keeps, in a folder workspace on a stopped WSL distro (E9)', () => {
it("checks the folder as the user's act when the user started it", async () => {
await failedStart(true)
expect(statsInUserAct).toEqual([true])
})
it('never touches the folder for a background start', async () => {
await failedStart(false)
expect(statMock).not.toHaveBeenCalled()
})
})
@@ -0,0 +1,128 @@
import type * as FsPromises from 'node:fs/promises'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const statMock = vi.hoisted(() => vi.fn<(path: string) => Promise<{ isDirectory(): boolean }>>())
vi.mock('node:fs/promises', async (importOriginal) => ({
...(await importOriginal<typeof FsPromises>()),
stat: statMock
}))
// Fragments stay side-effect ordered: mocks, then lifecycle, then fixtures.
const { OrcaRuntimeService } = await import('./orca-runtime-test-mocks.spec')
await import('./orca-runtime-test-lifecycle.spec')
const {
createFolderWorkspaceRuntimeStore,
makeFolderProjectGroup,
makeFolderWorkspace,
TEST_FOLDER_WORKSPACE_KEY
} = await import('./orca-runtime-test-fixtures.spec')
const { blockedWslDistroForPath } = await import('../wsl/wsl-unc-access-gate')
const { isWslUserActFor } = await import('../wsl/wsl-user-act-scope')
const { wslDistroHoldMessage } = await import('../../shared/wsl-distro-stopped-hold')
const { LOCAL_EXECUTION_HOST_ID } = await import('../../shared/execution-host')
const PARENT = '\\\\wsl.localhost\\Ubuntu\\home\\me'
const FOLDER = `${PARENT}\\notes`
const hostPlatform = process.platform
const statsInUserAct: boolean[] = []
function makeRuntime(): InstanceType<typeof OrcaRuntimeService> {
return new OrcaRuntimeService(
createFolderWorkspaceRuntimeStore(
makeFolderWorkspace({ folderPath: FOLDER }),
makeFolderProjectGroup({ parentPath: PARENT })
)
)
}
beforeEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
statsInUserAct.length = 0
statMock.mockReset()
statMock.mockImplementation(async () => {
statsInUserAct.push(isWslUserActFor('Ubuntu'))
return { isDirectory: () => true }
})
// Stopped, as the real gate reads it: only the user's act on the distro passes.
vi.mocked(blockedWslDistroForPath).mockImplementation(async () =>
isWslUserActFor('Ubuntu') ? null : { distro: 'Ubuntu', state: 'stopped' }
)
})
afterEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: hostPlatform })
vi.mocked(blockedWslDistroForPath).mockReset()
vi.mocked(blockedWslDistroForPath).mockResolvedValue(null)
})
describe('a runtime start in a folder workspace on a stopped WSL distro (E9)', () => {
it("checks the folder as the user's act, which may start the distro", async () => {
await expect(
makeRuntime().resolveTerminalWorkspaceLaunchScope(
`id:${TEST_FOLDER_WORKSPACE_KEY}`,
undefined,
{
wslUserAct: true
}
)
).resolves.toMatchObject({ id: TEST_FOLDER_WORKSPACE_KEY, path: FOLDER })
expect(statsInUserAct).toEqual([true])
})
it('treats a deduplicated CLI create as the user act too', async () => {
const run = vi.fn(async () => ({ handle: 'term-1' }))
await makeRuntime().dedupeTerminalCreate(
'local',
`id:${TEST_FOLDER_WORKSPACE_KEY}`,
'mutation-1',
false,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the dedupe returns `run`'s result untouched; only the launch scope is under test.
run as never,
{ wslUserAct: true }
)
expect(statsInUserAct).toEqual([true])
expect(run).toHaveBeenCalledOnce()
})
it('checks the folder as the user act for an agent-session create', async () => {
await makeRuntime()
.createAgentSession({
clientOperationId: `${Date.now()}-0123456789abcdef0123456789abcdef`,
worktree: `id:${TEST_FOLDER_WORKSPACE_KEY}`,
agent: 'claude'
})
// Later launch steps are out of scope; only the folder check's act is under test.
.catch(() => undefined)
expect(statsInUserAct[0]).toBe(true)
})
it('checks the folder as the user act for a resume the user asked for', async () => {
await makeRuntime().resumeAgentSessionInWorkspace(TEST_FOLDER_WORKSPACE_KEY, {
agent: 'claude',
providerSession: { key: 'session_id', id: 'provider-session-1' },
captureHostId: LOCAL_EXECUTION_HOST_ID,
mutationId: 'resume-1'
})
expect(statsInUserAct[0]).toBe(true)
})
it('checks the folder as the user act for an explicit agent-session ensure', async () => {
await makeRuntime()
.ensureAgentSession({
kind: 'explicit',
worktree: `id:${TEST_FOLDER_WORKSPACE_KEY}`,
agent: 'claude',
providerSession: { key: 'session_id', id: 'provider-session-1' }
})
// Later launch steps are out of scope; only the folder check's act is under test.
.catch(() => undefined)
expect(statsInUserAct[0]).toBe(true)
})
it('holds background work with Start, naming the distro, without touching the folder', async () => {
await expect(
makeRuntime().resolveTerminalWorkspaceLaunchScope(`id:${TEST_FOLDER_WORKSPACE_KEY}`)
).rejects.toThrow(wslDistroHoldMessage('Ubuntu', 'stopped'))
expect(statMock).not.toHaveBeenCalled()
})
})
@@ -106,7 +106,10 @@ export abstract class OrcaRuntimeWithGetAgentSessionExecutionNamespace extends O
if (!this.store) {
throw new Error('runtime_unavailable')
}
const workspace = await this.resolveTerminalWorkspaceLaunchScope(request.worktree)
// An explicit ensure is the caller's own act (automatic is refused above), as its spawn below.
const workspace = await this.resolveTerminalWorkspaceLaunchScope(request.worktree, undefined, {
wslUserAct: true
})
const namespace = this.getAgentSessionExecutionNamespace(workspace, request.agent)
if (
!namespace ||
@@ -9,6 +9,12 @@ import {
getFolderWorkspacePathStatus,
inferFolderWorkspacePathConnection
} from '../../project-groups/folder-workspace-path-status'
import {
blockedWslDistroForPath,
runAsWslUserAct,
wslDistroForPath
} from '../../wsl/wsl-unc-access-gate'
import { wslDistroHoldMessage } from '../../../shared/wsl-distro-stopped-hold'
import type { TerminalWorkspaceLaunchScope } from '../runtime-legacy-worker-terminal-recovery-types'
import { folderWorkspaceKey, parseWorkspaceKey } from '../../../shared/workspace-scope'
import type { ResolvedWorktree } from '../runtime-worktree-path-identity'
@@ -117,8 +123,10 @@ export abstract class OrcaRuntimeWithTransitionGraphReloadToTerminalState extend
return connection.kind === 'ssh' ? connection.connectionId : null
}
/** `wslUserAct`: the user's start, so its folder check may start a stopped WSL distro (E9). */
protected async resolveFolderWorkspaceLaunchScope(
selector: string
selector: string,
options: { wslUserAct?: boolean } = {}
): Promise<(TerminalWorkspaceLaunchScope & { folderWorkspace: FolderWorkspace }) | null> {
const workspace = this.resolveFolderWorkspaceSelector(selector)
if (!workspace) {
@@ -127,11 +135,24 @@ export abstract class OrcaRuntimeWithTransitionGraphReloadToTerminalState extend
if (!this.store) {
throw new Error('runtime_unavailable')
}
const status = await getFolderWorkspacePathStatus(this.store, {
scope: 'folder-workspace',
folderWorkspaceId: workspace.id
})
assertFolderWorkspacePathUsable(status)
const store = this.store
const readStatus = () =>
getFolderWorkspacePathStatus(store, {
scope: 'folder-workspace',
folderWorkspaceId: workspace.id
})
if (options.wslUserAct) {
assertFolderWorkspacePathUsable(
await runAsWslUserAct(wslDistroForPath(workspace.folderPath), readStatus)
)
} else {
// Background work holds with Start (a pane's card, an automation's hold), never "unavailable".
const blocked = await blockedWslDistroForPath(workspace.folderPath)
if (blocked) {
throw new Error(wslDistroHoldMessage(blocked.distro, blocked.state))
}
assertFolderWorkspacePathUsable(await readStatus())
}
return {
id: folderWorkspaceKey(workspace.id),
path: workspace.folderPath,
@@ -225,7 +225,9 @@ export abstract class OrcaRuntimeWithRestoredPaneStart extends OrcaRuntimeWithBu
if (resumeStartup === undefined && tab.launchAgent) {
let workspace
try {
workspace = await this.resolveTerminalWorkspaceLaunchScope(`id:${worktreeId}`)
workspace = await this.resolveTerminalWorkspaceLaunchScope(`id:${worktreeId}`, undefined, {
wslUserAct: wslUserAct === true
})
agentStartup = await this.resolveMobileSessionTerminalCommand(workspace, {
agent: tab.launchAgent
})
@@ -109,7 +109,10 @@ export abstract class OrcaRuntimeWithResumeAgentSessionInWorkspace extends OrcaR
}
let workspace
try {
workspace = await this.resolveTerminalWorkspaceLaunchScope(`id:${worktreeId}`)
// A resume the user asked for, as its create below.
workspace = await this.resolveTerminalWorkspaceLaunchScope(`id:${worktreeId}`, undefined, {
wslUserAct: true
})
} catch {
return refuse('no_resume_command')
}
@@ -187,14 +187,17 @@ export abstract class OrcaRuntimeWithFailedStartPanes extends OrcaRuntimeWithCre
worktreeSelector: string,
created: dependencies.Worktree | undefined,
pane: FailedStartPane,
{ title }: dependencies.TerminalCreateOptions,
{ title, wslUserAct }: dependencies.TerminalCreateOptions,
error: unknown
): Promise<void> {
if (error instanceof PaneOwnerContentionError) {
// Another owner's terminal runs in the pane; the window attaches to it.
return
}
const workspace = await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector, created)
// The failed start's own act: a user's start may wake the distro; background stays held.
const workspace = await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector, created, {
wslUserAct: wslUserAct === true
})
const create = { type: 'createTerminalTab' as const, workspace: workspace.id, ...pane }
const reply = await this.applyLayoutCommand(title ? { ...create, title } : create)
// `tab_exists`: the start wrote this pane before failing and kept it.
@@ -53,7 +53,13 @@ export abstract class OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal ext
opts: OwnedMobileTerminalCreateOptions = {}
): Promise<RuntimeMobileSessionCreateTerminalResult> {
this.pruneOwnedMobileDispatchRecipes()
const workspace = await this.resolveTerminalWorkspaceLaunchScope(`id:${worktreeId}`)
const workspace = await this.resolveTerminalWorkspaceLaunchScope(
`id:${worktreeId}`,
undefined,
{
wslUserAct: opts.wslUserAct === true
}
)
const cwd = this.resolveWorkspaceTerminalStartupCwd(workspace, opts.cwd)
// Why: an SSH sessionId means reattach; only synthesize local serve ids so SSH fresh terminals still call pty.spawn.
const stableSessionId =
@@ -133,7 +139,8 @@ export abstract class OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal ext
`id:${worktreeId}`,
opts.createMutation.id,
true,
(_selector, handle) => create(handle)
(_selector, handle) => create(handle),
{ wslUserAct: opts.wslUserAct === true }
)
: await create()
if (terminal.ptyId) {
@@ -63,7 +63,10 @@ export abstract class OrcaRuntimeWithRunCreateMobileSessionTerminal extends Orca
): Promise<RuntimeMobileSessionCreateTerminalResult> {
const pairedCreate = Boolean(opts.clientNavigationId)
const graphEpoch = this.captureReadyGraphEpoch()
const workspace = await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector)
// The paired client's own create: its user's act, as every start below.
const workspace = await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector, undefined, {
wslUserAct: true
})
const worktreeId = workspace.id
const cwd = this.resolveWorkspaceTerminalStartupCwd(workspace, opts.cwd)
assertMobileTerminalShellSupported(this.store, workspace, opts.shellOverride, cwd)
@@ -14,8 +14,21 @@ import {
} from '../../../../shared/execution-host'
import { resolveWorktreeLaunchHost } from '../../worktree-launch-host-repo'
import type { TuiAgent } from '../../../../shared/tui-agent'
import type { Worktree } from '../../../../shared/worktree/types'
import type { TerminalWorkspaceLaunchScope } from '../../runtime-legacy-worker-terminal-recovery-types'
export abstract class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRuntimeWithCreateAgentSession {
/** A create's scope: its `wslUserAct` lets the folder check start a stopped WSL distro (E9). */
protected resolveTerminalCreateLaunchScope(
selector: string,
createdWorktree: Worktree | undefined,
opts: { wslUserAct?: boolean }
): Promise<TerminalWorkspaceLaunchScope> {
return this.resolveTerminalWorkspaceLaunchScope(selector, createdWorktree, {
wslUserAct: opts.wslUserAct === true
})
}
async dedupeTerminalCreate(
clientIdentity: string,
worktreeSelector: string | undefined,
@@ -24,7 +37,8 @@ export abstract class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRun
run: (
canonicalWorktreeSelector: string | undefined,
preAllocatedHandle: string | undefined
) => Promise<RuntimeTerminalCreate>
) => Promise<RuntimeTerminalCreate>,
options: { wslUserAct?: boolean } = {}
): Promise<RuntimeTerminalCreate> {
if (!clientMutationId || !worktreeSelector) {
if (reconcileExisting) {
@@ -32,7 +46,11 @@ export abstract class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRun
}
return await run(worktreeSelector, undefined)
}
const workspace = await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector)
const workspace = await this.resolveTerminalWorkspaceLaunchScope(
worktreeSelector,
undefined,
options
)
const canonicalWorktreeSelector = `id:${workspace.id}`
const preAllocatedHandle = deriveRemoteRuntimeTerminalCreateHandle(
clientIdentity,
@@ -40,7 +40,7 @@ export abstract class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTermi
if (!ptyController?.spawn) {
throw new Error('runtime_unavailable')
}
const workspace = await this.resolveTerminalWorkspaceLaunchScope(worktreeSelector, created)
const workspace = await this.resolveTerminalCreateLaunchScope(worktreeSelector, created, opts)
const launchOpts = await this.resolveAgentTerminalCreateOptions(workspace, opts)
const launchTitleAt = { sequence: this.nextTitleObservationSequence(), epochMs: Date.now() }
let cwd = this.resolveWorkspaceTerminalStartupCwd(workspace, launchOpts.cwd) ?? workspace.path
@@ -98,7 +98,9 @@ export abstract class OrcaRuntimeWithSplitPaneInMain extends OrcaRuntimeWithSpli
return this.keepSplitThatCannotStart(source, opts, new Error('runtime_unavailable'))
}
const workspace = await this.resolveTerminalWorkspaceLaunchScope(
`id:${source.worktreeId}`
`id:${source.worktreeId}`,
undefined,
{ wslUserAct: opts.wslUserAct === true }
).catch((error: unknown) => this.keepSplitThatCannotStart(source, opts, error))
const preAllocatedHandle = this.createPreAllocatedTerminalHandle()
const result = await spawnInAdmittedPane(this.store, controller, {
@@ -47,7 +47,8 @@ export abstract class OrcaRuntimeWithSplitPtyBackedTerminal extends OrcaRuntimeW
const direction = opts.direction ?? 'horizontal'
const workspace = await this.resolveTerminalWorkspaceLaunchScope(
`id:${pty.worktreeId}`,
createdWorktree
createdWorktree,
{ wslUserAct: opts.wslUserAct === true }
)
const sourceAuthority = this.resolveTerminalSplitSourceAuthority(
workspace.id,
@@ -120,14 +120,17 @@ export abstract class OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktr
async resolveTerminalWorkspaceLaunchScope(
selector: string,
createdWorktree?: Worktree
createdWorktree?: Worktree,
options: { wslUserAct?: boolean } = {}
): Promise<TerminalWorkspaceLaunchScope> {
return (await this.resolveTerminalWorkspaceLaunchTarget(selector, createdWorktree)).scope
return (await this.resolveTerminalWorkspaceLaunchTarget(selector, createdWorktree, options))
.scope
}
protected async resolveTerminalWorkspaceLaunchTarget(
selector: string,
createdWorktree?: Worktree
createdWorktree?: Worktree,
options: { wslUserAct?: boolean } = {}
): Promise<ResolvedTerminalWorkspaceLaunchTarget> {
if (isFloatingWorkspaceSelector(selector)) {
// Why no managed worktree: the floating sentinel has no repo or worktree record behind it.
@@ -143,7 +146,7 @@ export abstract class OrcaRuntimeWithResolveBrowserNetworkExecutionHostForWorktr
}
}
const folderScope = await this.resolveFolderWorkspaceLaunchScope(selector)
const folderScope = await this.resolveFolderWorkspaceLaunchScope(selector, options)
if (folderScope) {
return {
scope: folderScope,
@@ -117,7 +117,9 @@ export async function consumePreparedBackgroundCommand(
'id:wt-1',
undefined,
false,
expect.any(Function)
expect.any(Function),
// terminal.create is the caller's own act, as its createTerminal (E9).
{ wslUserAct: true }
)
return result
}