mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
fix(launch-prompt): plan a host that cannot write its staging folder for main's delivery
d8cfcf397a refused an agent launch the daemon could not stage, which also
refused worker briefs (and AI-button prompts) that main pasted. The staged
script and the launch file share one folder per host, so "can write it" now
folds into takesLaunchFile, derived before planning in main: this machine's
temp folder (checked when planned, a refused write remembered for 30 s) or the
WSL distro's folder (the probe's failure, and a refused write, are remembered
for the same TTL). A host that cannot write it is planned for main's delivery
up front: the paste where main pasted, the line within the typed budget where
main typed it. Only a write that fails after the plan said yes is refused.
This commit is contained in:
@@ -18,10 +18,8 @@ import { getRecoveredHistorySeedSegments } from './terminal-history-seed-segment
|
||||
import { AGENT_SESSION_CLAIM_DAEMON_PROTOCOL_VERSION, type CreateOrAttachResult } from './types'
|
||||
import { normalizeWslColdRestoreCwd } from './wsl-cold-restore-cwd'
|
||||
import { resolveWslSessionContext } from './wsl-session-context'
|
||||
import {
|
||||
forgetWslLaunchDirectoryAfterRefusal,
|
||||
resolveSpawnWslLaunchDirectory
|
||||
} from '../providers/wsl-launch-directory-resolution'
|
||||
import { noteLaunchArtifactRefusal } from '../providers/local-launch-artifact-directory'
|
||||
import { resolveSpawnWslLaunchDirectory } from '../providers/wsl-launch-directory-resolution'
|
||||
import { resolveSafePtyDefaultCwd } from '../providers/pty-default-cwd'
|
||||
import { resolveUnixShellPath } from '../providers/local-pty-utils'
|
||||
import type { PtySpawnOptions, PtySpawnResult } from '../providers/types'
|
||||
@@ -265,7 +263,7 @@ export abstract class DaemonPtySessionSpawn extends DaemonPtySpawnResult {
|
||||
try {
|
||||
result = await this.createOrAttachSpawn(context, context.historySeedSegments)
|
||||
} catch (error) {
|
||||
forgetWslLaunchDirectoryAfterRefusal(wslLaunchDirectory, error)
|
||||
noteLaunchArtifactRefusal({ wslDistro, wslLaunchDirectory }, error)
|
||||
throw error
|
||||
}
|
||||
if (result.isNew && !attachOnly) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
localLaunchArtifactsWritable,
|
||||
noteLaunchArtifactRefusal
|
||||
} from './local-launch-artifact-directory'
|
||||
import { WSL_LAUNCH_DIRECTORY_FAILURE_TTL_MS } from './wsl-launch-directory-resolution'
|
||||
|
||||
vi.mock('../wsl/wsl-runner', () => ({ runWslProcess: vi.fn() }))
|
||||
|
||||
const REFUSAL = new Error(
|
||||
'Orca could not write it (ENOSPC), so the agent was not started. [launch_file_unavailable]'
|
||||
)
|
||||
|
||||
// Why: a host that cannot write its temp folder neither stages a line nor writes a launch file, so
|
||||
// the next launch is planned without either rather than refused again.
|
||||
describe('whether this machine can write its staging folder', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('leaves this machine’s folder alone when the refused write was into a WSL distro', () => {
|
||||
noteLaunchArtifactRefusal({ wslDistro: 'Ubuntu', wslLaunchDirectory: undefined }, REFUSAL)
|
||||
expect(localLaunchArtifactsWritable()).toBe(true)
|
||||
})
|
||||
|
||||
it('is yes until a write there is refused, then again after the failure TTL', () => {
|
||||
vi.useFakeTimers()
|
||||
expect(localLaunchArtifactsWritable()).toBe(true)
|
||||
noteLaunchArtifactRefusal({ wslDistro: null, wslLaunchDirectory: undefined }, new Error('EIO'))
|
||||
expect(localLaunchArtifactsWritable()).toBe(true)
|
||||
noteLaunchArtifactRefusal({ wslDistro: null, wslLaunchDirectory: undefined }, REFUSAL)
|
||||
expect(localLaunchArtifactsWritable()).toBe(false)
|
||||
vi.advanceTimersByTime(WSL_LAUNCH_DIRECTORY_FAILURE_TTL_MS)
|
||||
expect(localLaunchArtifactsWritable()).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { accessSync, constants } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { isLaunchFileRefusal } from '../../shared/launch-prompt-file'
|
||||
import type { WslLaunchDirectory } from '../../shared/wsl-launch-directory'
|
||||
import {
|
||||
noteWslLaunchDirectoryRefusal,
|
||||
WSL_LAUNCH_DIRECTORY_FAILURE_TTL_MS
|
||||
} from './wsl-launch-directory-resolution'
|
||||
|
||||
let refusedAt: number | null = null
|
||||
|
||||
/** Whether this machine can write its temp folder, where staged lines and launch files go. A write
|
||||
* refused there counts for the failure TTL, then the folder is checked again. */
|
||||
export function localLaunchArtifactsWritable(now = Date.now()): boolean {
|
||||
if (refusedAt !== null && now - refusedAt < WSL_LAUNCH_DIRECTORY_FAILURE_TTL_MS) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
accessSync(tmpdir(), constants.W_OK)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/** A spawn refused for its staged line or launch file: the folder it wrote to counts as broken,
|
||||
* so the next launch is planned without writing there. */
|
||||
export function noteLaunchArtifactRefusal(
|
||||
spawn: {
|
||||
wslDistro: string | null | undefined
|
||||
wslLaunchDirectory: WslLaunchDirectory | undefined
|
||||
},
|
||||
error: unknown
|
||||
): void {
|
||||
if (spawn.wslDistro) {
|
||||
noteWslLaunchDirectoryRefusal(spawn.wslLaunchDirectory, error)
|
||||
} else if (isLaunchFileRefusal(error)) {
|
||||
refusedAt = Date.now()
|
||||
}
|
||||
}
|
||||
@@ -30,10 +30,8 @@ import {
|
||||
writeSpawnLaunchFile,
|
||||
type WrittenLaunchFile
|
||||
} from '../../shared/launch-file-writing'
|
||||
import {
|
||||
forgetWslLaunchDirectoryAfterRefusal,
|
||||
resolveSpawnWslLaunchDirectory
|
||||
} from './wsl-launch-directory-resolution'
|
||||
import { resolveSpawnWslLaunchDirectory } from './wsl-launch-directory-resolution'
|
||||
import { noteLaunchArtifactRefusal } from './local-launch-artifact-directory'
|
||||
|
||||
export async function spawnLocalPty(
|
||||
args: PtySpawnOptions,
|
||||
@@ -69,7 +67,7 @@ export async function spawnLocalPty(
|
||||
wslDirectory: wslLaunchDirectory
|
||||
})
|
||||
} catch (error) {
|
||||
forgetWslLaunchDirectoryAfterRefusal(wslLaunchDirectory, error)
|
||||
noteLaunchArtifactRefusal({ wslDistro, wslLaunchDirectory }, error)
|
||||
throw error
|
||||
}
|
||||
args = {
|
||||
@@ -84,7 +82,7 @@ export async function spawnLocalPty(
|
||||
return await spawnFreshLocalPty(args, getOptions, reattachId, launchFile)
|
||||
} catch (error) {
|
||||
removeLaunchFile(launchFile)
|
||||
forgetWslLaunchDirectoryAfterRefusal(wslLaunchDirectory, error)
|
||||
noteLaunchArtifactRefusal({ wslDistro, wslLaunchDirectory }, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,8 @@ import { createLocalPtyLaunchPlan, resolveLocalPtyWslDistro } from './local-pty-
|
||||
import type { LocalPtyProviderOptions } from './local-pty-provider-types'
|
||||
import type { PtySpawnOptions } from './types'
|
||||
import {
|
||||
forgetWslLaunchDirectoryAfterRefusal,
|
||||
noteWslLaunchDirectoryRefusal,
|
||||
wslLaunchDirectoryKnownBroken,
|
||||
resolveSpawnWslLaunchDirectory,
|
||||
resolveWslLaunchDirectory,
|
||||
WSL_LAUNCH_DIRECTORY_FAILURE_TTL_MS
|
||||
@@ -82,21 +83,38 @@ describe('resolveWslLaunchDirectory', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// Why (stack QA P8-2): a folder found once and broken later must not be written into forever.
|
||||
it('asks the distro again after a write into the found folder was refused', async () => {
|
||||
runWslProcess.mockResolvedValue(answers('/home/ada\n/bin/bash\n'))
|
||||
const found = await resolveWslLaunchDirectory('Void')
|
||||
forgetWslLaunchDirectoryAfterRefusal(found, new Error('EACCES'))
|
||||
await resolveWslLaunchDirectory('Void')
|
||||
expect(runWslProcess).toHaveBeenCalledTimes(1)
|
||||
forgetWslLaunchDirectoryAfterRefusal(
|
||||
found,
|
||||
new Error(
|
||||
'Orca could not write it (EEXIST), so the agent was not started. [launch_file_unavailable]'
|
||||
// Why (stack QA P8-2): a folder found once and broken later must not be written into forever,
|
||||
// and the next launch is planned without it until the distro is asked again.
|
||||
it('counts a folder broken after a refused write, then asks the distro again', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
runWslProcess.mockResolvedValue(answers('/home/ada\n/bin/bash\n'))
|
||||
const found = await resolveWslLaunchDirectory('Void')
|
||||
noteWslLaunchDirectoryRefusal(found, new Error('EACCES'))
|
||||
expect(wslLaunchDirectoryKnownBroken('Void')).toBe(false)
|
||||
noteWslLaunchDirectoryRefusal(
|
||||
found,
|
||||
new Error(
|
||||
'Orca could not write it (EEXIST), so the agent was not started. [launch_file_unavailable]'
|
||||
)
|
||||
)
|
||||
)
|
||||
await resolveWslLaunchDirectory('Void')
|
||||
expect(runWslProcess).toHaveBeenCalledTimes(2)
|
||||
expect(wslLaunchDirectoryKnownBroken('Void')).toBe(true)
|
||||
await expect(resolveWslLaunchDirectory('Void')).resolves.toBeUndefined()
|
||||
expect(runWslProcess).toHaveBeenCalledTimes(1)
|
||||
await vi.advanceTimersByTimeAsync(WSL_LAUNCH_DIRECTORY_FAILURE_TTL_MS)
|
||||
expect(wslLaunchDirectoryKnownBroken('Void')).toBe(false)
|
||||
await expect(resolveWslLaunchDirectory('Void')).resolves.toMatchObject({ distro: 'Void' })
|
||||
expect(runWslProcess).toHaveBeenCalledTimes(2)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('counts a distro whose probe failed broken, for planning', async () => {
|
||||
runWslProcess.mockResolvedValue(answers('', 1))
|
||||
await resolveWslLaunchDirectory('Mint')
|
||||
await Promise.resolve()
|
||||
expect(wslLaunchDirectoryKnownBroken('Mint')).toBe(true)
|
||||
})
|
||||
|
||||
it('probes only for a spawn that needs the directory', async () => {
|
||||
|
||||
@@ -11,6 +11,8 @@ import { runWslProcess } from '../wsl/wsl-runner'
|
||||
const ORCA_CACHE_RELATIVE = '.cache/orca'
|
||||
|
||||
const probes = new Map<string, Promise<WslLaunchDirectory | undefined>>()
|
||||
/** Distros whose folder a probe or a write found broken, until the failure TTL lets them retry. */
|
||||
const failedDistros = new Set<string>()
|
||||
|
||||
/** A failed probe is answered from memory this long, so a broken distro does not cost every spawn
|
||||
* a `wsl.exe` round trip, yet a distro that comes back is found again. */
|
||||
@@ -41,8 +43,8 @@ export function resolveSpawnWslLaunchDirectory(
|
||||
* The distro directory a WSL spawn's staged line and launch file are written to, and the login
|
||||
* shell the pane runs, which decides how a staged line is sourced; undefined when the distro cannot
|
||||
* be asked, and the write site then refuses a launch that needs one. Probed once per distro; a
|
||||
* success is kept until a write into it fails (`forgetWslLaunchDirectoryAfterRefusal`), a failure
|
||||
* for `WSL_LAUNCH_DIRECTORY_FAILURE_TTL_MS`.
|
||||
* success is kept until a write into it fails (`noteWslLaunchDirectoryRefusal`), a failure for
|
||||
* `WSL_LAUNCH_DIRECTORY_FAILURE_TTL_MS`.
|
||||
*/
|
||||
export async function resolveWslLaunchDirectory(
|
||||
distro: string | null | undefined
|
||||
@@ -56,26 +58,41 @@ export async function resolveWslLaunchDirectory(
|
||||
probes.set(distro, probe)
|
||||
const pending = probe
|
||||
void pending.then((found) => {
|
||||
if (!found) {
|
||||
setTimeout(() => {
|
||||
if (probes.get(distro) === pending) {
|
||||
probes.delete(distro)
|
||||
}
|
||||
}, WSL_LAUNCH_DIRECTORY_FAILURE_TTL_MS).unref?.()
|
||||
if (found) {
|
||||
failedDistros.delete(distro)
|
||||
} else {
|
||||
rememberFailure(distro, pending)
|
||||
}
|
||||
})
|
||||
}
|
||||
return await probe
|
||||
}
|
||||
|
||||
/** A spawn into this directory was refused: forget it, so the next launch asks the distro again
|
||||
* rather than writing into a folder that stopped working. */
|
||||
export function forgetWslLaunchDirectoryAfterRefusal(
|
||||
function rememberFailure(distro: string, pending: Promise<WslLaunchDirectory | undefined>): void {
|
||||
failedDistros.add(distro)
|
||||
setTimeout(() => {
|
||||
if (probes.get(distro) === pending) {
|
||||
probes.delete(distro)
|
||||
failedDistros.delete(distro)
|
||||
}
|
||||
}, WSL_LAUNCH_DIRECTORY_FAILURE_TTL_MS).unref?.()
|
||||
}
|
||||
|
||||
/** Whether the distro's folder is known broken, so a launch is planned without writing there. */
|
||||
export function wslLaunchDirectoryKnownBroken(distro: string): boolean {
|
||||
return failedDistros.has(distro)
|
||||
}
|
||||
|
||||
/** A spawn into this directory was refused: count it broken until the failure TTL, so the next
|
||||
* launch is planned without it and a later one asks the distro again. */
|
||||
export function noteWslLaunchDirectoryRefusal(
|
||||
directory: WslLaunchDirectory | undefined,
|
||||
error: unknown
|
||||
): void {
|
||||
if (directory && isLaunchFileRefusal(error)) {
|
||||
probes.delete(directory.distro)
|
||||
const pending = Promise.resolve(undefined)
|
||||
probes.set(directory.distro, pending)
|
||||
rememberFailure(directory.distro, pending)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,7 +187,8 @@ export class OrcaRuntimeWithCreateAgentSession extends OrcaRuntimeWithGetAgentSe
|
||||
host: thisOrcaLaunchHost({
|
||||
launchPlatform: startupArgs.platform,
|
||||
isRemote: Boolean(workspace.connectionId),
|
||||
settings
|
||||
settings,
|
||||
workspacePath: workspace.path
|
||||
}),
|
||||
// Why: this create returns before the agent is ready, so nothing pastes after it.
|
||||
paste: 'never'
|
||||
|
||||
@@ -61,7 +61,12 @@ export class OrcaRuntimeWithResolveMobileSessionTerminalCommand extends OrcaRunt
|
||||
isRemote
|
||||
}),
|
||||
prompt: opts.agentPrompt ?? '',
|
||||
host: thisOrcaLaunchHost({ launchPlatform, isRemote, settings }),
|
||||
host: thisOrcaLaunchHost({
|
||||
launchPlatform,
|
||||
isRemote,
|
||||
settings,
|
||||
workspacePath: workspace.path
|
||||
}),
|
||||
// Why: a quick command has no paste after ready.
|
||||
paste: 'never'
|
||||
})
|
||||
|
||||
@@ -275,7 +275,8 @@ export class OrcaRuntimeWithResolveWorktreeRemovalTarget extends OrcaRuntimeWith
|
||||
launchPlatform: platform,
|
||||
isRemote,
|
||||
settings,
|
||||
windowsShellOverride: opts.shellOverride
|
||||
windowsShellOverride: opts.shellOverride,
|
||||
workspacePath: workspace.path
|
||||
}),
|
||||
paste: opts.onStartupPromptCarry ? 'when-host-proves-agent' : 'never'
|
||||
})
|
||||
|
||||
@@ -161,7 +161,8 @@ export function buildWorktreeStartupForAgent(
|
||||
host: thisOrcaLaunchHost({
|
||||
isRemote: repoIsRemote(repo),
|
||||
launchPlatform: environment.getLaunchPlatform(),
|
||||
settings
|
||||
settings,
|
||||
workspacePath: repo.path
|
||||
}),
|
||||
// The caller, or else the host's follow-up, pastes what the line leaves once the agent runs.
|
||||
paste: 'when-host-proves-agent'
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cachedPwshAvailability } from '../pwsh'
|
||||
import { localLaunchArtifactsWritable } from '../providers/local-launch-artifact-directory'
|
||||
import { wslLaunchDirectoryKnownBroken } from '../providers/wsl-launch-directory-resolution'
|
||||
import { thisOrcaLaunchHost } from './this-orca-launch-host'
|
||||
|
||||
vi.mock('../pwsh', () => ({ cachedPwshAvailability: vi.fn(() => null) }))
|
||||
vi.mock('../providers/local-launch-artifact-directory', () => ({
|
||||
localLaunchArtifactsWritable: vi.fn(() => true)
|
||||
}))
|
||||
vi.mock('../providers/wsl-launch-directory-resolution', () => ({
|
||||
wslLaunchDirectoryKnownBroken: vi.fn(() => false)
|
||||
}))
|
||||
|
||||
const realPlatform = process.platform
|
||||
|
||||
@@ -51,3 +59,44 @@ describe('the PowerShell a launch this Orca runs lands in', () => {
|
||||
).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
// Why: a host that cannot write its staging folder can neither stage a line nor write a launch
|
||||
// file there, so the launch is planned for main's delivery up front instead of refused.
|
||||
describe('whether a launch this Orca runs can write its staging folder', () => {
|
||||
afterEach(() => {
|
||||
onPlatform(realPlatform)
|
||||
vi.mocked(localLaunchArtifactsWritable).mockReturnValue(true)
|
||||
vi.mocked(wslLaunchDirectoryKnownBroken).mockReturnValue(false)
|
||||
})
|
||||
|
||||
it('follows this machine’s temp folder for a local launch', () => {
|
||||
const host = () =>
|
||||
thisOrcaLaunchHost({ launchPlatform: 'darwin', isRemote: false, settings: null })
|
||||
onPlatform('darwin')
|
||||
expect(host().takesLaunchFile).toBe(true)
|
||||
vi.mocked(localLaunchArtifactsWritable).mockReturnValue(false)
|
||||
expect(host().takesLaunchFile).toBe(false)
|
||||
})
|
||||
|
||||
it('follows the distro’s folder for a WSL workspace', () => {
|
||||
onPlatform('win32')
|
||||
vi.mocked(wslLaunchDirectoryKnownBroken).mockImplementation((distro) => distro === 'qasfwin')
|
||||
const host = (workspacePath: string) =>
|
||||
thisOrcaLaunchHost({
|
||||
launchPlatform: 'linux',
|
||||
isRemote: false,
|
||||
settings: null,
|
||||
workspacePath
|
||||
})
|
||||
expect(host('\\\\wsl.localhost\\qasfwin\\root\\fixture').takesLaunchFile).toBe(false)
|
||||
expect(host('\\\\wsl.localhost\\Ubuntu\\home\\ada').takesLaunchFile).toBe(true)
|
||||
})
|
||||
|
||||
it('cannot check an SSH host’s folder from here', () => {
|
||||
vi.mocked(localLaunchArtifactsWritable).mockReturnValue(false)
|
||||
expect(
|
||||
thisOrcaLaunchHost({ launchPlatform: 'linux', isRemote: true, settings: null })
|
||||
.takesLaunchFile
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { cachedPwshAvailability } from '../pwsh'
|
||||
import { parseWslUncPath } from '../../shared/wsl-paths'
|
||||
import { localLaunchArtifactsWritable } from '../providers/local-launch-artifact-directory'
|
||||
import { wslLaunchDirectoryKnownBroken } from '../providers/wsl-launch-directory-resolution'
|
||||
import {
|
||||
describeLaunchHost,
|
||||
spawnedWindowsShell,
|
||||
@@ -13,6 +16,8 @@ export function thisOrcaLaunchHost(args: {
|
||||
settings: WindowsShellSettings
|
||||
/** The shell this launch asked for (`--shell`), which outranks the setting. */
|
||||
windowsShellOverride?: string | null
|
||||
/** Where the pane opens; a WSL path names the distro its line and file are written into. */
|
||||
workspacePath?: string
|
||||
}): LaunchHost {
|
||||
const local = !args.isRemote && args.launchPlatform === 'win32' && process.platform === 'win32'
|
||||
return describeLaunchHost({
|
||||
@@ -26,6 +31,24 @@ export function thisOrcaLaunchHost(args: {
|
||||
windowsShellOverride: args.windowsShellOverride,
|
||||
pwshAvailable: cachedPwshAvailability()
|
||||
})
|
||||
: null
|
||||
: null,
|
||||
writesLaunchArtifacts: writesLaunchArtifacts(args)
|
||||
})
|
||||
}
|
||||
|
||||
/** Whether this host can write the folder a staged line and a launch file go in. An SSH host's is
|
||||
* the relay's, which this client cannot check. */
|
||||
function writesLaunchArtifacts(args: {
|
||||
launchPlatform: NodeJS.Platform
|
||||
isRemote: boolean
|
||||
workspacePath?: string
|
||||
}): boolean {
|
||||
if (args.isRemote) {
|
||||
return true
|
||||
}
|
||||
if (process.platform === 'win32' && args.launchPlatform !== 'win32') {
|
||||
const distro = args.workspacePath ? parseWslUncPath(args.workspacePath)?.distro : undefined
|
||||
return !distro || !wslLaunchDirectoryKnownBroken(distro)
|
||||
}
|
||||
return localLaunchArtifactsWritable()
|
||||
}
|
||||
|
||||
@@ -21,9 +21,10 @@ export type LaunchHost = {
|
||||
/** Whether the host can prove the launched agent holds its terminal before a paste
|
||||
* (`launched-agent-foreground`). A Windows host cannot, so #24257's guarded paste is refused. */
|
||||
provesAgentInFront: boolean
|
||||
/** Whether the host writes a launch file the agent can read. A paired Orca may be older, and an
|
||||
* SSH Windows host's relay may run its panes in WSL (its OpenSSH default shell, which this client
|
||||
* cannot see), where it writes none. Such a host gets the line or the paste instead. */
|
||||
/** Whether the host writes a launch file the agent can read, and stages a long line, both in one
|
||||
* folder. A paired Orca may be older, an SSH Windows host's relay may run its panes in WSL (its
|
||||
* OpenSSH default shell, which this client cannot see), and a host may find that folder
|
||||
* unwritable. Such a host gets the line within its typed budget, or the paste, instead. */
|
||||
takesLaunchFile: boolean
|
||||
/** The shell a local Windows pane is spawned as, which the line is judged and quoted by: a WSL
|
||||
* pane runs the distro's POSIX shell whatever the Windows setting, and the PowerShell decides
|
||||
@@ -80,12 +81,17 @@ export function describeLaunchHost(args: {
|
||||
paired: boolean
|
||||
/** `spawnedWindowsShell` on this machine, which a launch elsewhere does not use. */
|
||||
windowsPaneShell?: WindowsPaneShell | null
|
||||
/** Whether the host can write the folder its staged lines and launch files go in. */
|
||||
writesLaunchArtifacts?: boolean
|
||||
}): LaunchHost {
|
||||
const runsElsewhere = args.isRemote || args.paired
|
||||
return {
|
||||
paired: args.paired,
|
||||
provesAgentInFront: (runsElsewhere ? args.launchPlatform : args.hostPlatform) !== 'win32',
|
||||
takesLaunchFile: !args.paired && !(args.isRemote && args.launchPlatform === 'win32'),
|
||||
takesLaunchFile:
|
||||
!args.paired &&
|
||||
!(args.isRemote && args.launchPlatform === 'win32') &&
|
||||
args.writesLaunchArtifacts !== false,
|
||||
windowsPaneShell:
|
||||
runsElsewhere || args.launchPlatform !== 'win32' ? null : (args.windowsPaneShell ?? null)
|
||||
}
|
||||
|
||||
@@ -218,6 +218,28 @@ describe('a host that types the line raw', () => {
|
||||
expect(plan('claude', 'fix it', extra)?.carry).toBe('on-line')
|
||||
})
|
||||
|
||||
// Why: a host that cannot write its staging folder types a long line raw, so the plan picks
|
||||
// main's delivery up front: the paste where main pasted, the line where main typed it.
|
||||
it('plans a host that cannot stage for main’s delivery, not a staged line or a file', () => {
|
||||
const extra = {
|
||||
platform: 'linux' as const,
|
||||
host: {
|
||||
paired: false,
|
||||
provesAgentInFront: true,
|
||||
takesLaunchFile: false,
|
||||
windowsPaneShell: null
|
||||
}
|
||||
}
|
||||
const long = 'y'.repeat(20_000)
|
||||
expect(plan('claude', 'fix it', extra)?.carry).toBe('on-line')
|
||||
expect(plan('claude', long, { ...extra, paste: 'once-agent-runs' })?.carry).toBe(
|
||||
'paste-after-ready'
|
||||
)
|
||||
expect(plan('claude', long, extra)?.carry).toBe('paste-after-ready')
|
||||
expect(plan('codex', 'first line\nsecond line', extra)?.carry).toBe('paste-after-ready')
|
||||
expect(plan('claude', long, { ...extra, paste: 'never' })?.carry).toBe('on-line')
|
||||
})
|
||||
|
||||
it('pastes on a paired host what its line cannot carry typed, even for Claude', () => {
|
||||
const extra = {
|
||||
platform: 'linux' as const,
|
||||
|
||||
@@ -104,10 +104,11 @@ const utf8 = new TextEncoder()
|
||||
* stages it when it is long or multi-line, and a Windows host types it when its shell was measured
|
||||
* carrying such a line exactly (`windowsLaunchLineVerdict`); an unmeasured one does what main does
|
||||
* on the caller's path. When the line cannot (past the argv ceiling, measured damaged by a Windows
|
||||
* shell, past a paired host's typed budget), a caller whose paste main used gets that paste, so the
|
||||
* agent receives the user's text. Otherwise it rides a launch file, which goes only to an agent measured reading
|
||||
* one, on a host that writes it. Failing a file it is pasted once the agent is ready, unless the
|
||||
* caller's paste cannot reach the agent on this host: then the line carries it, as main typed it.
|
||||
* shell, past the typed budget of a paired host or one that cannot write its staging folder), a
|
||||
* caller whose paste main used gets that paste, so the agent receives the user's text. Otherwise it
|
||||
* rides a launch file, which goes only to an agent measured reading one, on a host that writes it.
|
||||
* Failing a file it is pasted once the agent is ready, unless the caller's paste cannot reach the
|
||||
* agent on this host: then the line carries it, as main typed it.
|
||||
*/
|
||||
export function carryLaunchPrompt<A extends CarriedPlanArgs, P extends { launchCommand: string }>(
|
||||
args: A,
|
||||
@@ -180,11 +181,11 @@ export function carryLaunchPrompt<A extends CarriedPlanArgs, P extends { launchC
|
||||
paneShell === 'powershell.exe' || paneShell === 'pwsh.exe' ? paneShell : null
|
||||
)
|
||||
: 'exact'
|
||||
// Why #24257's typed budget: an older paired host may type the line raw, truncated past it.
|
||||
if (
|
||||
windowsLine === 'damaged' ||
|
||||
(args.host.paired && !typedStartupLineFits(plan.launchCommand))
|
||||
) {
|
||||
// Why #24257's typed budget: an older paired host, or a host that cannot write its staging
|
||||
// folder, types the line raw, truncated past it.
|
||||
const stagesOnHost = args.platform !== 'win32' || paneShell === 'wsl.exe'
|
||||
const typesRaw = args.host.paired || (!args.host.takesLaunchFile && stagesOnHost)
|
||||
if (windowsLine === 'damaged' || (typesRaw && !typedStartupLineFits(plan.launchCommand))) {
|
||||
return viaLaunchFile()
|
||||
}
|
||||
// Why: unmeasured, so this path does what main does: its paste, or the line it typed.
|
||||
|
||||
Reference in New Issue
Block a user