mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
fix(omp): start new tasks without auto-resuming old sessions (#20622)
* wip(omp): prove fresh settings overlay without redirecting storage * fix(omp): guard fresh launches with execution-host settings * fix(omp): preserve unmodelled shell launch commands * test(omp): consolidate shell fixture path import * preserve fresh OMP launch status * test: cover preserved OMP launch status * fix: recognize wrapped fresh OMP launches * chore(ci): refresh validation against fixed main baseline * fix(omp): recognize generated fresh launch guards across shells * fix(omp): preserve draft status and clear prefill across Unix shells * fix(omp): run cmd draft cleanup after either guard branch * fix(omp): launch drafts safely with nounset enabled * fix(omp): select draft shell without parser diagnostics * test(omp): await relay environment augmentation
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
# Fresh OMP launches
|
||||
|
||||
Orca's new-session and draft launch plans apply a one-time `--config` overlay
|
||||
containing `autoResume: false`. OMP's configured session directory, settings,
|
||||
authentication and extensions remain in their usual locations. Saved launch
|
||||
configuration omits the overlay so explicit resume keeps its normal semantics.
|
||||
Custom commands with session selectors, unknown flags, positional arguments or
|
||||
shell compounds are left unchanged.
|
||||
|
||||
The execution host creates the overlay. Local and WSL terminals use Orca userData
|
||||
(with WSLENV path translation); SSH relays use their own managed directory. Config
|
||||
creation is independent of status-hook preferences and does not require plugin
|
||||
source installation. An unavailable file produces a terminal diagnostic and skips
|
||||
OMP. Filesystem failures do not prevent unrelated agents or bare shells starting.
|
||||
|
||||
The guard invokes OMP in the current shell, preserving functions, aliases and the
|
||||
managed status wrapper. A nonzero agent exit never triggers a second launch.
|
||||
POSIX commands also support fish; environment presence is checked before expansion
|
||||
so an old host under `set -u` reports the same missing-settings diagnostic.
|
||||
|
||||
## Mixed versions
|
||||
|
||||
The existing command and environment transport carries the launch unchanged; no
|
||||
new RPC or stream opcode is introduced. A new host exports the path before shell
|
||||
startup, including bare shells that receive an OMP command later. An old client
|
||||
continues its existing launch behavior on a new host. A new fresh-launch command
|
||||
on an old host without the managed environment fails visibly and requests a host
|
||||
update and terminal restart. It must not silently fall back to OMP auto-resume.
|
||||
|
||||
## Verification
|
||||
|
||||
`src/shared/omp-fresh-launch-shell.test.ts` runs actual available bash, zsh and fish
|
||||
shells, checking exact argv, a single invocation, nonzero exit, deleted settings
|
||||
and an absent environment variable. `src/relay/omp-fresh-launch-environment.test.ts`
|
||||
checks the guarded command through relay environment assembly and the actual OMP
|
||||
shell wrapper, retaining both extension and config arguments and prefill.
|
||||
Local host assembly tests recognize guarded POSIX, cmd and PowerShell commands.
|
||||
|
||||
Run the actual OMP storage smoke against a read-only OMP checkout:
|
||||
|
||||
```sh
|
||||
ORCA_BACKGROUND_LAUNCH=1 bun tests/tools/omp-fresh-session-runtime-smoke.mjs /path/to/oh-my-pi
|
||||
```
|
||||
|
||||
For Windows, bundle `tests/tools/omp-fresh-launch-windows-smoke.ts` with
|
||||
`bun build --target=node --outfile=/tmp/omp-fresh-windows-smoke.mjs`, transfer the
|
||||
bundle to the host and run it using Node with `ORCA_BACKGROUND_LAUNCH=1`.
|
||||
The smoke uses temporary files and process-local environment only. Both cmd and
|
||||
PowerShell must pass existing/missing/unset/directory settings cases, preserving
|
||||
exit 17 for the single successful launch and returning exit 1 without launching
|
||||
when settings are unavailable. This passed on Windows host `awin` on 2026-09-14.
|
||||
@@ -129,6 +129,7 @@ export const agentHookServerModuleMock = () => ({
|
||||
export const piTitlebarExtensionModuleMock = () => ({
|
||||
piTitlebarExtensionService: {
|
||||
buildPtyEnv: piBuildPtyEnvMock,
|
||||
buildFreshOmpEnv: () => ({ ORCA_OMP_FRESH_CONFIG: '/tmp/orca-fresh-session.yml' }),
|
||||
clearPty: piClearPtyMock
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { withFreshOmpLaunch } from '../../shared/omp-fresh-launch'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
readFileSyncMock,
|
||||
@@ -293,14 +294,19 @@ describe('registerPtyHandlers', () => {
|
||||
})
|
||||
expect(env.ORCA_PI_SOURCE_AGENT_DIR).toBe('/tmp/default-pi-agent')
|
||||
})
|
||||
it('threads command: "omp" through to piBuildPtyEnv and emits OMP status metadata', async () => {
|
||||
it.each([
|
||||
'omp',
|
||||
withFreshOmpLaunch('omp', 'posix'),
|
||||
withFreshOmpLaunch('omp', 'powershell'),
|
||||
withFreshOmpLaunch('omp', 'cmd')
|
||||
])('threads OMP command %s through to the host integration', async (command) => {
|
||||
// Why: OMP launches emit ORCA_OMP_* shadow vars, not Pi-named ones; only PI_CODING_AGENT_DIR stays (OMP's own binary reads it).
|
||||
const env = await spawnAndGetEnv(
|
||||
undefined,
|
||||
{ PI_CODING_AGENT_DIR: '/tmp/user-omp-agent' },
|
||||
undefined,
|
||||
undefined,
|
||||
'omp'
|
||||
command
|
||||
)
|
||||
expect(piBuildPtyEnvMock).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { withFreshOmpLaunch } from '../../shared/omp-fresh-launch'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { spawnMock } from './pty-ipc-mock-registry'
|
||||
import { BUNDLED_CLI_PATH, TEST_CODEX_HOME, makeDisposable } from './pty-ipc-test-constants'
|
||||
@@ -58,6 +59,22 @@ describe('registerPtyHandlers', () => {
|
||||
const { handlers, mainWindow, spawnAndGetEnv, withBundledCli } = setupPtyIpcSuite()
|
||||
|
||||
describe('spawn environment', () => {
|
||||
it('prepares fresh OMP settings even when status hooks are disabled', () => {
|
||||
const env = buildPtyHostEnv(
|
||||
'fresh-without-hooks',
|
||||
{ ORCA_OMP_FRESH_CONFIG: '/other-host/stale.yml' },
|
||||
{
|
||||
isPackaged: true,
|
||||
userDataPath: '/tmp/orca-user-data',
|
||||
selectedCodexHomePath: null,
|
||||
agentStatusHooksEnabled: false,
|
||||
launchCommand: withFreshOmpLaunch('omp', 'posix')
|
||||
}
|
||||
)
|
||||
expect(env.ORCA_OMP_FRESH_CONFIG).toBe('/tmp/orca-fresh-session.yml')
|
||||
expect(env.ORCA_OMP_STATUS_EXTENSION).toBeUndefined()
|
||||
})
|
||||
|
||||
it('routes headless browser launches through the owning Orca workspace', () => {
|
||||
const inheritedBrowser = process.env.BROWSER
|
||||
delete process.env.BROWSER
|
||||
|
||||
@@ -194,6 +194,9 @@ export function buildPtyHostEnv(
|
||||
overlay: 'ORCA_OMP_CODING_AGENT_DIR',
|
||||
source: 'ORCA_OMP_SOURCE_AGENT_DIR'
|
||||
})
|
||||
if (shouldPrepareOmpShadow) {
|
||||
Object.assign(baseEnv, piTitlebarExtensionService.buildFreshOmpEnv())
|
||||
}
|
||||
delete baseEnv.ORCA_OMP_STATUS_EXTENSION
|
||||
delete baseEnv.ORCA_PRIME_AGENT_SOURCE_AGENT_DIR
|
||||
delete baseEnv.ORCA_PRIME_AGENT_STATUS_EXTENSION
|
||||
|
||||
@@ -275,6 +275,8 @@ describe('PiTitlebarExtensionService', () => {
|
||||
const content = 'agent.db credentials'
|
||||
|
||||
expect(env.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(readFileSync(env.ORCA_OMP_FRESH_CONFIG, 'utf8')).toBe('autoResume: false\n')
|
||||
expect(env.ORCA_OMP_FRESH_CONFIG.startsWith(userDataDir)).toBe(true)
|
||||
expect(env.ORCA_OMP_SOURCE_AGENT_DIR).toBe(piHome)
|
||||
expect(env.ORCA_OMP_STATUS_EXTENSION).toBe(join(piHome, 'extensions', 'orca-agent-status.ts'))
|
||||
expect(existsSync(sourcePath)).toBe(false)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { materializeOmpFreshConfig } from '../../shared/omp-fresh-config'
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { homedir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
@@ -174,16 +175,24 @@ export class PiTitlebarExtensionService {
|
||||
}
|
||||
}
|
||||
|
||||
buildFreshOmpEnv(): Record<string, string> {
|
||||
return {
|
||||
ORCA_OMP_FRESH_CONFIG: materializeOmpFreshConfig(
|
||||
join(getAppEnvironment().getPath('userData'), OMP_MANAGED_STATUS_EXTENSION_DIR)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
buildPtyEnv(
|
||||
ptyId: string,
|
||||
existingAgentDir: string | undefined,
|
||||
kind: PiAgentKind,
|
||||
options?: { materializeDefaultHome?: boolean; configDirName?: string }
|
||||
): Record<string, string> {
|
||||
// The caller resolves the effective launch environment. Reading the
|
||||
// daemon's ambient PI_CONFIG_DIR here can select the host profile for a
|
||||
// guest/WSL launch whose environment has not been hydrated yet.
|
||||
const sourceAgentDir = existingAgentDir || getDefaultPiAgentDir(kind, options?.configDirName)
|
||||
const freshConfigEnv = kind === 'omp' ? this.buildFreshOmpEnv() : {}
|
||||
// The caller resolves the effective launch environment before this point.
|
||||
const sourceAgentDir =
|
||||
existingAgentDir || getDefaultPiAgentDir(kind, options?.configDirName)
|
||||
if (kind !== 'prime-agent') {
|
||||
try {
|
||||
this.safeRemoveOverlay(this.getPtyOverlayDir(ptyId, kind), kind)
|
||||
@@ -203,7 +212,9 @@ export class PiTitlebarExtensionService {
|
||||
if (kind === 'omp') {
|
||||
const statusSource = withOrcaManagedExtensionMarker(getPiAgentStatusExtensionSource(kind))
|
||||
const statusExtensionPath = this.writeOmpFallbackStatusExtension(statusSource)
|
||||
return statusExtensionPath ? { ORCA_OMP_STATUS_EXTENSION: statusExtensionPath } : {}
|
||||
return statusExtensionPath
|
||||
? { ...freshConfigEnv, ORCA_OMP_STATUS_EXTENSION: statusExtensionPath }
|
||||
: freshConfigEnv
|
||||
}
|
||||
return {}
|
||||
}
|
||||
@@ -213,7 +224,7 @@ export class PiTitlebarExtensionService {
|
||||
}
|
||||
|
||||
const installed = this.installManagedExtensions(sourceAgentDir, kind)
|
||||
const env: Record<string, string> = {}
|
||||
const env: Record<string, string> = { ...freshConfigEnv }
|
||||
if (kind === 'omp') {
|
||||
env.ORCA_OMP_SOURCE_AGENT_DIR = installed.sourceAgentDir
|
||||
if (installed.statusExtensionPath) {
|
||||
|
||||
@@ -63,6 +63,7 @@ describe('addOrcaWslInteropEnv', () => {
|
||||
ORCA_USER_DATA_PATH: 'C:\\Users\\jin\\AppData\\Roaming\\Orca',
|
||||
ORCA_CLI_COMMAND: 'orca-ide',
|
||||
ORCA_CODEX_LAUNCH_PREFLIGHT: 'C:\\Program Files\\Orca\\resources\\bin\\orca.exe',
|
||||
ORCA_OMP_FRESH_CONFIG: 'C:\\Orca\\fresh-session.yml',
|
||||
ORCA_OMP_STATUS_EXTENSION: 'C:\\Users\\jin\\.omp\\agent\\extensions\\orca-agent-status.ts',
|
||||
ORCA_PRIME_AGENT_STATUS_EXTENSION: 'C:\\stale\\orca-agent-status.ts',
|
||||
ORCA_PANE_KEY: 'tab-1:leaf-1',
|
||||
@@ -87,6 +88,7 @@ describe('addOrcaWslInteropEnv', () => {
|
||||
expect(env.WSLENV).toContain('ORCA_CLI_COMMAND/u')
|
||||
expect(env.WSLENV).toContain('ORCA_CODEX_LAUNCH_PREFLIGHT/p')
|
||||
expect(env.WSLENV).toContain('ORCA_OMP_STATUS_EXTENSION/p')
|
||||
expect(env.WSLENV).toContain('ORCA_OMP_FRESH_CONFIG/p')
|
||||
expect(env.WSLENV).not.toContain('ORCA_PRIME_AGENT_STATUS_EXTENSION')
|
||||
expect(env.WSLENV).toContain('ORCA_PANE_KEY/u')
|
||||
expect(env.WSLENV).toContain('ORCA_TAB_ID/u')
|
||||
|
||||
@@ -99,8 +99,8 @@ export function addOrcaWslInteropEnv(env: Record<string, string>): void {
|
||||
'ORCA_WSL_HOOK_INSTANCE/u',
|
||||
'ORCA_OMP_SOURCE_AGENT_DIR/p',
|
||||
'ORCA_OMP_STATUS_EXTENSION/p',
|
||||
// A protocol name, never a path; in-guest agents read it to pick an image encoder.
|
||||
`${ORCA_IMAGE_PROTOCOL_ENV}/u`,
|
||||
'ORCA_OMP_FRESH_CONFIG/p',
|
||||
...worktreeSetupWslenvEntries(env)
|
||||
]
|
||||
applyWslenvPassthrough(env, passthroughEntries)
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { runProcess } from '../shared/child-process/run-process'
|
||||
import { getPosixOmpShellWrapper } from '../main/pty/omp-shell-wrapper'
|
||||
import { mkdtempSync, readFileSync, rmSync, existsSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import type * as NodeOs from 'node:os'
|
||||
import { delimiter, join } from 'node:path'
|
||||
import { afterEach, expect, it, vi } from 'vitest'
|
||||
import { RelayDispatcher } from './dispatcher'
|
||||
import { PtyHandler } from './pty-handler'
|
||||
import { RelayAgentHookRuntime } from './relay-agent-hook-runtime'
|
||||
import { PluginOverlayManager } from './plugin-overlay'
|
||||
import { withFreshOmpLaunch } from '../shared/omp-fresh-launch'
|
||||
|
||||
const state = vi.hoisted(() => ({ home: '' }))
|
||||
vi.mock('node:os', async (original) => ({
|
||||
...(await original<typeof NodeOs>()),
|
||||
homedir: () => state.home
|
||||
}))
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
if (state.home) {
|
||||
rmSync(state.home, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
it('prepares the execution host OMP config and status extension for a guarded launch', async () => {
|
||||
state.home = mkdtempSync(join(tmpdir(), 'orca-relay-fresh-'))
|
||||
const dispatcher = new RelayDispatcher(() => {})
|
||||
const handler = new PtyHandler(dispatcher)
|
||||
const augment = vi.spyOn(handler, 'addEnvAugmenter')
|
||||
const source = vi.spyOn(PluginOverlayManager.prototype, 'hasPiSource').mockReturnValue(true)
|
||||
const original = PluginOverlayManager.prototype.materializePi
|
||||
vi.spyOn(PluginOverlayManager.prototype, 'materializePi').mockImplementation(function (
|
||||
this: PluginOverlayManager,
|
||||
...args
|
||||
) {
|
||||
this.setSources({ ompExtensionSource: '// Orca managed test status extension' })
|
||||
return original.apply(this, args)
|
||||
})
|
||||
const runtime = new RelayAgentHookRuntime(
|
||||
dispatcher,
|
||||
handler,
|
||||
join(state.home, 'relay.sock'),
|
||||
join(state.home, 'hooks')
|
||||
)
|
||||
await runtime.start()
|
||||
try {
|
||||
const environment = await augment.mock.calls[1][0]({
|
||||
id: 'fresh-pane',
|
||||
shell: '/bin/bash',
|
||||
env: { HOME: state.home },
|
||||
command: withFreshOmpLaunch('omp', 'posix')
|
||||
})
|
||||
expect(readFileSync(environment.ORCA_OMP_FRESH_CONFIG, 'utf8')).toBe('autoResume: false\n')
|
||||
expect(existsSync(environment.ORCA_OMP_STATUS_EXTENSION)).toBe(true)
|
||||
expect(environment.ORCA_OMP_SOURCE_AGENT_DIR).toBe(join(state.home, '.omp', 'agent'))
|
||||
expect(environment.PI_CODING_AGENT_DIR).toBeUndefined()
|
||||
expect(environment.ORCA_PI_SOURCE_AGENT_DIR).toBeUndefined()
|
||||
if (process.platform !== 'win32' && existsSync('/bin/bash')) {
|
||||
writeFileSync(
|
||||
join(state.home, 'omp'),
|
||||
'#!/bin/sh\nprintf "%s\\n" "$@" "$ORCA_OMP_PREFILL"\nexit 17\n',
|
||||
{ mode: 0o755 }
|
||||
)
|
||||
const result = await runProcess({
|
||||
program: '/bin/bash',
|
||||
args: [
|
||||
'--noprofile',
|
||||
'--norc',
|
||||
'-c',
|
||||
`${getPosixOmpShellWrapper()}\n${withFreshOmpLaunch('omp', 'posix')}`
|
||||
],
|
||||
cwd: state.home,
|
||||
env: {
|
||||
...process.env,
|
||||
...environment,
|
||||
PATH: state.home + delimiter + process.env.PATH,
|
||||
ORCA_OMP_PREFILL: 'remote draft'
|
||||
}
|
||||
})
|
||||
expect(result.code).toBe(17)
|
||||
expect(result.stdout.trim().split('\n')).toEqual([
|
||||
'--extension',
|
||||
environment.ORCA_OMP_STATUS_EXTENSION,
|
||||
'--config',
|
||||
environment.ORCA_OMP_FRESH_CONFIG,
|
||||
'remote draft'
|
||||
])
|
||||
}
|
||||
|
||||
source.mockReturnValue(false)
|
||||
expect(
|
||||
augment.mock.calls[1][0]({ id: 'other', shell: '/bin/bash', env: {}, command: 'codex' })
|
||||
).toEqual({})
|
||||
} finally {
|
||||
runtime.stop()
|
||||
dispatcher.dispose()
|
||||
}
|
||||
})
|
||||
|
||||
it('fails the config guard safely when the relay-owned directory cannot be created', () => {
|
||||
state.home = mkdtempSync(join(tmpdir(), 'orca-relay-fresh-fail-'))
|
||||
const blocked = join(state.home, 'file')
|
||||
writeFileSync(blocked, '')
|
||||
const manager = new PluginOverlayManager({ homeDir: blocked })
|
||||
expect(manager.materializeOmpFreshConfig()).toBe('')
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
import { materializeOmpFreshConfig } from '../shared/omp-fresh-config'
|
||||
// Why: relay-side equivalent of Orca's local agent integration installers.
|
||||
// OpenCode still needs a config overlay, while Pi/OMP now get Orca-managed
|
||||
// extension files installed into the remote agent homes. Host paths from the
|
||||
@@ -275,6 +276,12 @@ export class PluginOverlayManager {
|
||||
}
|
||||
}
|
||||
|
||||
materializeOmpFreshConfig(): string {
|
||||
return materializeOmpFreshConfig(
|
||||
join(this.homeDir, RELAY_HOOKS_DIR, OMP_MANAGED_STATUS_EXTENSION_DIR)
|
||||
)
|
||||
}
|
||||
|
||||
/** Install the Pi/OMP status extension into the remote real agent dir.
|
||||
* `kind` selects which Pi-compatible agent's default dir to use when
|
||||
* `existingAgentDir` is not supplied.
|
||||
|
||||
@@ -94,9 +94,6 @@ export class RelayAgentHookRuntime {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!this.pluginOverlay.hasPiSource()) {
|
||||
return env
|
||||
}
|
||||
const launchCommandHint = resolveSetupAgentSequenceLaunchCommand(context.env, context.command)
|
||||
const explicitKind = isPiCompatibleAgentType(context.launchAgent)
|
||||
? context.launchAgent
|
||||
@@ -106,6 +103,12 @@ export class RelayAgentHookRuntime {
|
||||
const kind = explicitKind ?? 'pi'
|
||||
const hasLaunchCommand =
|
||||
typeof launchCommandHint === 'string' && launchCommandHint.trim().length > 0
|
||||
if (kind === 'omp' || !hasLaunchCommand) {
|
||||
env.ORCA_OMP_FRESH_CONFIG = this.pluginOverlay.materializeOmpFreshConfig()
|
||||
}
|
||||
if (!this.pluginOverlay.hasPiSource()) {
|
||||
return env
|
||||
}
|
||||
if (kind === 'pi') {
|
||||
const sourceDir = resolvePiSourceAgentDir(context.env, context.shell, 'pi')
|
||||
const result = this.pluginOverlay.materializePi(overlayId, sourceDir, 'pi', {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { withFreshOmpLaunch } from './omp-fresh-launch'
|
||||
import { buildAgentDraftLaunchPlan } from './tui-agent-startup'
|
||||
import { detectExplicitPiAgentKindFromCommand } from './pi-agent-kind'
|
||||
import {
|
||||
isAgentForegroundWrapperProcess,
|
||||
isExpectedAgentProcess,
|
||||
@@ -16,6 +19,42 @@ describe('agent process recognition', () => {
|
||||
expect(isRecognizedAgentType('codex-aarch64-ap')).toBe(true)
|
||||
})
|
||||
|
||||
it.each(['posix', 'powershell', 'cmd'] as const)(
|
||||
'recognizes fresh OMP commands and drafts in %s',
|
||||
(shell) => {
|
||||
for (const command of ['omp', 'omp launch', 'omp --model example']) {
|
||||
const generated = withFreshOmpLaunch(command, shell)
|
||||
expect(recognizeAgentProcessFromCommandLine(generated)).toEqual({
|
||||
agent: 'omp',
|
||||
processName: 'omp'
|
||||
})
|
||||
expect(detectExplicitPiAgentKindFromCommand(generated)).toBe('omp')
|
||||
}
|
||||
const draft = buildAgentDraftLaunchPlan({
|
||||
agent: 'omp',
|
||||
draft: 'task',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux',
|
||||
shell
|
||||
})
|
||||
expect(recognizeAgentProcessFromCommandLine(draft?.launchCommand)).toEqual({
|
||||
agent: 'omp',
|
||||
processName: 'omp'
|
||||
})
|
||||
expect(detectExplicitPiAgentKindFromCommand(draft?.launchCommand)).toBe('omp')
|
||||
}
|
||||
)
|
||||
|
||||
it('does not classify embedded fresh-launch text as an agent command', () => {
|
||||
for (const command of [
|
||||
`echo 'omp --config "$ORCA_OMP_FRESH_CONFIG"'`,
|
||||
`echo '${withFreshOmpLaunch('omp', 'posix')}'`
|
||||
]) {
|
||||
expect(recognizeAgentProcessFromCommandLine(command)).toBeNull()
|
||||
expect(detectExplicitPiAgentKindFromCommand(command)).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it('recognizes the OpenClaude foreground process', () => {
|
||||
expect(recognizeAgentProcess('/usr/local/bin/openclaude')).toEqual({
|
||||
agent: 'openclaude',
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { AgentType } from './agent-status-types'
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
import { filterHeadlessOneShotAgentCommand } from './agent-headless-command'
|
||||
import { getFirstCommandToken } from './command-token-scanner'
|
||||
import { isFreshOmpLaunchCommand } from './omp-fresh-launch'
|
||||
|
||||
export type RecognizedAgentProcess = { agent: TuiAgent; processName: string }
|
||||
|
||||
@@ -286,6 +287,9 @@ export function recognizeAgentProcessFromCommandLine(
|
||||
if (!commandLine) {
|
||||
return null
|
||||
}
|
||||
if (isFreshOmpLaunchCommand(commandLine)) {
|
||||
return recognizedAgentForProcess('omp')
|
||||
}
|
||||
const keep = options?.includeHeadlessOneShot === true
|
||||
const tokens = tokenizeCommandLine(commandLine)
|
||||
const firstNormalized = normalizeProcessName(tokens[0])
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdtemp, writeFile, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, join } from 'node:path'
|
||||
import { expect, it } from 'vitest'
|
||||
import { runProcess } from './child-process/run-process'
|
||||
import { buildAgentDraftLaunchPlan } from './tui-agent-startup'
|
||||
|
||||
const shells = ['sh', 'bash', 'zsh', 'dash', 'ksh', 'fish'].filter(
|
||||
(shell) =>
|
||||
process.platform !== 'win32' &&
|
||||
(process.env.PATH ?? '')
|
||||
.split(delimiter)
|
||||
.some((directory) => existsSync(join(directory, shell)))
|
||||
)
|
||||
|
||||
it.each(
|
||||
shells.flatMap((shell) =>
|
||||
shell === 'fish'
|
||||
? [{ shell, nounset: false }]
|
||||
: [false, true].map((nounset) => ({ shell, nounset }))
|
||||
)
|
||||
)(
|
||||
'clears the draft and preserves status in $shell (nounset=$nounset)',
|
||||
async ({ shell, nounset }) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-omp-draft-'))
|
||||
try {
|
||||
const config = join(root, 'fresh settings.yml')
|
||||
const calls = join(root, 'calls')
|
||||
await writeFile(config, 'autoResume: false\n')
|
||||
const plan = buildAgentDraftLaunchPlan({
|
||||
agent: 'omp',
|
||||
draft: 'task with spaces',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux'
|
||||
})
|
||||
if (!plan) {
|
||||
throw new Error('Expected draft plan')
|
||||
}
|
||||
const define =
|
||||
shell === 'fish'
|
||||
? 'function omp; printf "%s\\n" "$ORCA_OMP_PREFILL" >> "$CAPTURE"; printf agent-stderr >&2; return 17; end; '
|
||||
: 'omp() { printf "%s\\n" "$ORCA_OMP_PREFILL" >> "$CAPTURE"; printf agent-stderr >&2; return 17; }; '
|
||||
const observe =
|
||||
shell === 'fish'
|
||||
? '; set -l result $status; set -q ORCA_OMP_PREFILL; and exit 91; exit $result'
|
||||
: '; result=$?; test -z "${ORCA_OMP_PREFILL+x}" || exit 91; exit "$result"'
|
||||
for (const present of [true, false]) {
|
||||
if (!present) {
|
||||
await rm(config)
|
||||
}
|
||||
const result = await runProcess({
|
||||
program: shell,
|
||||
args: ['-c', (nounset ? 'set -u; ' : '') + define + plan.launchCommand + observe],
|
||||
cwd: root,
|
||||
env: { ...process.env, ...plan.env, ORCA_OMP_FRESH_CONFIG: config, CAPTURE: calls }
|
||||
})
|
||||
expect(result.code, result.stderr).toBe(present ? 17 : 1)
|
||||
expect(result.stderr).toContain(
|
||||
present ? 'agent-stderr' : 'fresh OMP settings are unavailable'
|
||||
)
|
||||
if (present) {
|
||||
expect(result.stderr).toBe('agent-stderr')
|
||||
}
|
||||
expect(result.stderr).not.toContain('command substitutions not allowed')
|
||||
expect(await readFile(calls, 'utf8')).toBe('task with spaces\n')
|
||||
}
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
import { mkdtemp, writeFile, readFile, rm, mkdir } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { expect, it } from 'vitest'
|
||||
import { runProcess } from './child-process/run-process'
|
||||
import { buildAgentDraftLaunchPlan } from './tui-agent-startup'
|
||||
|
||||
it.skipIf(process.platform !== 'win32').each(['cmd', 'powershell'] as const)(
|
||||
'clears the draft and preserves status in native %s',
|
||||
async (shell) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-omp-draft-'))
|
||||
try {
|
||||
const config = join(root, 'fresh (%ORCA_EXPANSION_PROBE%) & settings!.yml')
|
||||
const calls = join(root, 'calls')
|
||||
await writeFile(
|
||||
join(root, 'omp.cmd'),
|
||||
'@echo off\r\necho %ORCA_OMP_PREFILL%>>"%CAPTURE%"\r\nexit /b 17\r\n'
|
||||
)
|
||||
const plan = buildAgentDraftLaunchPlan({
|
||||
agent: 'omp',
|
||||
draft: 'task with spaces',
|
||||
cmdOverrides: {},
|
||||
platform: 'win32',
|
||||
shell
|
||||
})
|
||||
if (!plan) {
|
||||
throw new Error('Expected draft plan')
|
||||
}
|
||||
for (const state of ['present', 'missing', 'unset', 'directory']) {
|
||||
await rm(config, { force: true, recursive: true })
|
||||
await rm(calls, { force: true })
|
||||
if (state === 'present') {
|
||||
await writeFile(config, 'autoResume: false\n')
|
||||
}
|
||||
if (state === 'directory') {
|
||||
await mkdir(config)
|
||||
}
|
||||
const env = {
|
||||
...process.env,
|
||||
...plan.env,
|
||||
ORCA_OMP_FRESH_CONFIG: state === 'unset' ? undefined : config,
|
||||
CAPTURE: calls,
|
||||
ORCA_EXPANSION_PROBE: 'unexpected'
|
||||
}
|
||||
const result =
|
||||
shell === 'cmd'
|
||||
? await runProcess({
|
||||
program: 'cmd.exe',
|
||||
args: ['/d', '/q'],
|
||||
cwd: root,
|
||||
env,
|
||||
input: `${plan.launchCommand}\r\nset "orca_result=%errorlevel%"\r\nif defined ORCA_OMP_PREFILL exit 91\r\nexit %orca_result%\r\n`
|
||||
})
|
||||
: await runProcess({
|
||||
program: 'powershell.exe',
|
||||
args: [
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
`function omp { Add-Content -LiteralPath $env:CAPTURE -Value $env:ORCA_OMP_PREFILL; $global:LASTEXITCODE = 17 }; ${plan.launchCommand}; $result = $LASTEXITCODE; if (Test-Path Env:ORCA_OMP_PREFILL) { exit 91 }; exit $result`
|
||||
],
|
||||
cwd: root,
|
||||
env
|
||||
})
|
||||
expect(result.code, JSON.stringify({ state, ...result })).toBe(state === 'present' ? 17 : 1)
|
||||
if (state === 'present') {
|
||||
expect((await readFile(calls, 'utf8')).trim()).toBe('task with spaces')
|
||||
} else {
|
||||
await expect(readFile(calls)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
clearEnvCommand,
|
||||
commandSeparator,
|
||||
quoteStartupArg,
|
||||
type AgentStartupShell
|
||||
} from './tui-agent-startup-shell'
|
||||
|
||||
const FUNCTION_NAME = '__orca_omp_draft'
|
||||
export const OMP_DRAFT_LAUNCH_PREFIX = `eval 'builtin --query set' 2>/dev/null && eval 'function ${FUNCTION_NAME}; `
|
||||
|
||||
/** Clear the calling shell's prefill without replacing the agent's exit status. */
|
||||
export function withOmpDraftCleanup(command: string, shell: AgentStartupShell): string {
|
||||
if (shell !== 'posix') {
|
||||
// cmd otherwise binds the cleanup to the guard's else branch.
|
||||
const launch = shell === 'cmd' ? `( ${command} )` : command
|
||||
return `${launch}${commandSeparator(shell)}${clearEnvCommand('ORCA_OMP_PREFILL', shell)}`
|
||||
}
|
||||
const fish = `function ${FUNCTION_NAME}; ${command}; set -l __orca_status $status; set -e -g ORCA_OMP_PREFILL; return $__orca_status; end`
|
||||
const posix = `${FUNCTION_NAME}() { ${command}; set -- "$?"; unset ORCA_OMP_PREFILL; return "$1"; }`
|
||||
// Fish supports builtin --query; other shells reject it without parsing the wrong definition.
|
||||
return `eval 'builtin --query set' 2>/dev/null && eval ${quoteStartupArg(fish, 'posix')} || eval ${quoteStartupArg(posix, 'posix')}; ${FUNCTION_NAME}`
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { OMP_FRESH_CONFIG_FILENAME, OMP_FRESH_CONFIG_SOURCE } from './omp-fresh-launch'
|
||||
|
||||
/** An empty path fails the launch guard without preventing unrelated terminal launches. */
|
||||
export function materializeOmpFreshConfig(directory: string): string {
|
||||
try {
|
||||
const configPath = join(directory, OMP_FRESH_CONFIG_FILENAME)
|
||||
if (existsSync(configPath) && readFileSync(configPath, 'utf8') === OMP_FRESH_CONFIG_SOURCE) {
|
||||
return configPath
|
||||
}
|
||||
mkdirSync(directory, { recursive: true })
|
||||
writeFileSync(configPath, OMP_FRESH_CONFIG_SOURCE)
|
||||
return configPath
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdtemp, writeFile, readFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, join } from 'node:path'
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { runProcess } from './child-process/run-process'
|
||||
import { buildAgentStartupPlan } from './tui-agent-startup'
|
||||
|
||||
const roots: string[] = []
|
||||
afterEach(async () => {
|
||||
for (const root of roots.splice(0)) {
|
||||
await rm(root, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
const shells = ['bash', 'zsh', 'fish'].filter(
|
||||
(shell) =>
|
||||
process.platform !== 'win32' &&
|
||||
(process.env.PATH ?? '')
|
||||
.split(delimiter)
|
||||
.some((directory) => existsSync(join(directory, shell)))
|
||||
)
|
||||
it.each(shells)(
|
||||
'preserves same-shell function argv, one launch and exit status in %s',
|
||||
async (shell) => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-fresh-shell-'))
|
||||
roots.push(root)
|
||||
const config = join(root, 'fresh settings.yml')
|
||||
const capture = join(root, 'argv')
|
||||
await writeFile(config, 'autoResume: false\n')
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'omp',
|
||||
prompt: 'task with spaces',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux',
|
||||
isRemote: true
|
||||
})
|
||||
expect(plan).not.toBeNull()
|
||||
const define =
|
||||
shell === 'fish'
|
||||
? 'function omp; printf "%s\\n" $argv >> "$CAPTURE"; return 17; end; '
|
||||
: 'omp() { printf "%s\\n" "$@" >> "$CAPTURE"; return 17; }; '
|
||||
const result = await runProcess({
|
||||
program: shell,
|
||||
args: ['-c', define + plan!.launchCommand],
|
||||
cwd: root,
|
||||
env: { ...process.env, ORCA_OMP_FRESH_CONFIG: config, CAPTURE: capture }
|
||||
})
|
||||
expect(result.code).toBe(17)
|
||||
expect(await readFile(capture, 'utf8')).toBe(`--config\n${config}\ntask with spaces\n`)
|
||||
await rm(capture)
|
||||
await rm(config)
|
||||
const missing = await runProcess({
|
||||
program: shell,
|
||||
args: ['-c', define + plan!.launchCommand],
|
||||
cwd: root,
|
||||
env: { ...process.env, ORCA_OMP_FRESH_CONFIG: config, CAPTURE: capture }
|
||||
})
|
||||
expect(missing.code).not.toBe(0)
|
||||
expect(missing.stderr).toContain('fresh OMP settings are unavailable')
|
||||
await expect(readFile(capture)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
const unsetEnv: NodeJS.ProcessEnv = { ...process.env, CAPTURE: capture }
|
||||
delete unsetEnv.ORCA_OMP_FRESH_CONFIG
|
||||
const unset = await runProcess({
|
||||
program: shell,
|
||||
args: ['-c', (shell === 'fish' ? '' : 'set -u; ') + define + plan!.launchCommand],
|
||||
cwd: root,
|
||||
env: unsetEnv
|
||||
})
|
||||
expect(unset.code).not.toBe(0)
|
||||
expect(unset.stderr).toContain('fresh OMP settings are unavailable')
|
||||
await expect(readFile(capture)).rejects.toMatchObject({ code: 'ENOENT' })
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { withFreshOmpLaunch } from './omp-fresh-launch'
|
||||
import { buildAgentStartupPlan } from './tui-agent-startup'
|
||||
|
||||
describe('OMP fresh launch intent', () => {
|
||||
it.each(['omp', 'omp launch', 'omp --model provider/model', 'omp --config user.yml'])(
|
||||
'%s adds the final overlay',
|
||||
(command) => {
|
||||
expect(withFreshOmpLaunch(command, 'posix')).toContain(
|
||||
`${command} --config "$ORCA_OMP_FRESH_CONFIG"`
|
||||
)
|
||||
}
|
||||
)
|
||||
it.each([
|
||||
'omp --resume id',
|
||||
'omp -r id',
|
||||
'omp --continue',
|
||||
'omp -c',
|
||||
'omp --session-dir /custom',
|
||||
'omp --no-session',
|
||||
'omp --fork id',
|
||||
'omp models',
|
||||
'omp config',
|
||||
'omp wt',
|
||||
'omp --help',
|
||||
'omp --unknown foo',
|
||||
'omp --model',
|
||||
'omp -- hello',
|
||||
'echo omp',
|
||||
'omp && echo hi',
|
||||
'omp --model foo;',
|
||||
'omp --model $(preferred-model)',
|
||||
'omp --model `preferred-model`'
|
||||
])('preserves %s', (command) => {
|
||||
expect(withFreshOmpLaunch(command, 'posix')).toBe(command)
|
||||
})
|
||||
it.each(['cmd', 'powershell'] as const)('preserves compound values in %s', (shell) => {
|
||||
const command = 'omp --model foo&'
|
||||
expect(withFreshOmpLaunch(command, shell)).toBe(command)
|
||||
})
|
||||
it('quotes the host config path for each Windows shell', () => {
|
||||
expect(withFreshOmpLaunch('omp', 'powershell')).toContain(
|
||||
'omp --config "$env:ORCA_OMP_FRESH_CONFIG"'
|
||||
)
|
||||
expect(withFreshOmpLaunch('omp', 'cmd')).toContain('omp --config "%ORCA_OMP_FRESH_CONFIG%"')
|
||||
})
|
||||
it('keeps fresh intent out of saved resume command and environment', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'omp',
|
||||
prompt: 'new task',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux'
|
||||
})
|
||||
expect(plan?.launchCommand).toContain('--config "$ORCA_OMP_FRESH_CONFIG"')
|
||||
expect(JSON.stringify(plan?.launchConfig)).not.toContain('ORCA_OMP_FRESH_CONFIG')
|
||||
expect(plan?.env).toBeUndefined()
|
||||
})
|
||||
it('requires host-owned configuration for fresh SSH launches', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'omp',
|
||||
prompt: 'new task',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux',
|
||||
isRemote: true
|
||||
})
|
||||
expect(plan?.launchCommand).toContain('test -f "$ORCA_OMP_FRESH_CONFIG"')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
import { OMP_DRAFT_LAUNCH_PREFIX } from './omp-draft-launch'
|
||||
import { tokenizeStartupCommand, type AgentStartupShell } from './tui-agent-startup-shell'
|
||||
|
||||
export const ORCA_OMP_FRESH_CONFIG_ENV = 'ORCA_OMP_FRESH_CONFIG'
|
||||
export const OMP_FRESH_CONFIG_FILENAME = 'fresh-session.yml'
|
||||
export const OMP_FRESH_CONFIG_SOURCE = 'autoResume: false\n'
|
||||
|
||||
// Unknown flags may consume values or select a subcommand; leave those commands intact.
|
||||
const VALUE_FLAGS = new Set([
|
||||
'--model',
|
||||
'--provider',
|
||||
'--thinking',
|
||||
'--config',
|
||||
'--profile',
|
||||
'--extension',
|
||||
'-e',
|
||||
'--system-prompt',
|
||||
'--append-system-prompt',
|
||||
'--tools',
|
||||
'--skill',
|
||||
'--theme',
|
||||
'--api-key'
|
||||
])
|
||||
const SWITCH_FLAGS = new Set(['--no-extensions', '--no-skills', '--no-prompt-templates'])
|
||||
|
||||
/** Apply fresh intent to one launch command, never the saved resume configuration. */
|
||||
export function withFreshOmpLaunch(command: string, shell: AgentStartupShell, suffix = ''): string {
|
||||
const parsed = tokenizeStartupCommand(command, shell)
|
||||
if (!parsed.ok || parsed.spans.some((span) => span.divergesFromShell)) {
|
||||
return command + suffix
|
||||
}
|
||||
const executable = parsed.tokens[0]?.split(/[\\/]/).at(-1)?.toLowerCase()
|
||||
if (!['omp', 'omp.exe', 'omp.cmd', 'omp.bat', 'omp.sh', 'omp.js'].includes(executable ?? '')) {
|
||||
return command + suffix
|
||||
}
|
||||
let index = parsed.tokens[1] === 'launch' ? 2 : 1
|
||||
for (; index < parsed.tokens.length; index++) {
|
||||
const token = parsed.tokens[index]
|
||||
const equals = token.indexOf('=')
|
||||
const flag = equals === -1 ? token : token.slice(0, equals)
|
||||
if (VALUE_FLAGS.has(flag)) {
|
||||
if (equals === -1 && ++index >= parsed.tokens.length) {
|
||||
return command + suffix
|
||||
}
|
||||
} else if (!SWITCH_FLAGS.has(token)) {
|
||||
return command + suffix
|
||||
}
|
||||
}
|
||||
const path =
|
||||
shell === 'cmd'
|
||||
? `"%${ORCA_OMP_FRESH_CONFIG_ENV}%"`
|
||||
: shell === 'powershell'
|
||||
? `"$env:${ORCA_OMP_FRESH_CONFIG_ENV}"`
|
||||
: `"$${ORCA_OMP_FRESH_CONFIG_ENV}"`
|
||||
const launch = `${command} --config ${path}${suffix}`
|
||||
const error =
|
||||
'Orca: fresh OMP settings are unavailable on this host. Restart the terminal after updating Orca.'
|
||||
if (shell === 'powershell') {
|
||||
return `if (${path} -and (Test-Path -LiteralPath ${path} -PathType Leaf)) { ${launch} } else { $global:LASTEXITCODE = 1; Write-Error '${error}' }`
|
||||
}
|
||||
if (shell === 'cmd') {
|
||||
return `if exist ${path} (if not exist "${path.slice(1, -1)}\\*" (${launch}) else (echo ${error} 1>&2 & verify invalid 2>nul)) else (echo ${error} 1>&2 & verify invalid 2>nul)`
|
||||
}
|
||||
const available = `printenv ${ORCA_OMP_FRESH_CONFIG_ENV} >/dev/null && test -f ${path}`
|
||||
// && is supported by bash, zsh, and fish; invoking directly preserves shell aliases/functions.
|
||||
return `${available} || printf '%s\\n' '${error}' >&2; ${available} && ${launch}`
|
||||
}
|
||||
|
||||
/** Recognize only the host-settings guards generated by this module. */
|
||||
export function isFreshOmpLaunchCommand(command: string | undefined): boolean {
|
||||
if (command?.startsWith(OMP_DRAFT_LAUNCH_PREFIX)) {
|
||||
return true
|
||||
}
|
||||
const guarded = command?.startsWith('( ') ? command.slice(2) : command
|
||||
return Boolean(
|
||||
guarded &&
|
||||
[
|
||||
`printenv ${ORCA_OMP_FRESH_CONFIG_ENV} >/dev/null && test -f "$${ORCA_OMP_FRESH_CONFIG_ENV}"`,
|
||||
`if ("$env:${ORCA_OMP_FRESH_CONFIG_ENV}" -and (Test-Path -LiteralPath "$env:${ORCA_OMP_FRESH_CONFIG_ENV}" -PathType Leaf))`,
|
||||
`if exist "%${ORCA_OMP_FRESH_CONFIG_ENV}%" (if not exist "%${ORCA_OMP_FRESH_CONFIG_ENV}%\\*"`
|
||||
].some((prefix) => guarded.startsWith(prefix))
|
||||
)
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isFreshOmpLaunchCommand } from './omp-fresh-launch'
|
||||
import { TUI_AGENT_CONFIG } from './tui-agent-config'
|
||||
import { getCommandTokenPathBasename, getFirstCommandToken } from './command-token-scanner'
|
||||
|
||||
@@ -49,6 +50,9 @@ const PRIME_AGENT_LAUNCH_BINARY = getLaunchBinary(TUI_AGENT_CONFIG['prime-agent'
|
||||
export function detectExplicitPiAgentKindFromCommand(
|
||||
command: string | undefined
|
||||
): PiAgentKind | null {
|
||||
if (isFreshOmpLaunchCommand(command)) {
|
||||
return 'omp'
|
||||
}
|
||||
const binary = getLaunchBinary(command ?? '')
|
||||
if (binary === OMP_LAUNCH_BINARY) {
|
||||
return 'omp'
|
||||
|
||||
@@ -666,9 +666,7 @@ describe('tui agent startup plans', () => {
|
||||
expect(plan).not.toBeNull()
|
||||
expect(plan?.env).toEqual({ ORCA_OMP_PREFILL: 'fix the omp regression' })
|
||||
expect(plan?.expectedProcess).toBe('omp')
|
||||
expect(plan?.launchCommand).toBe(
|
||||
`omp; command test -n "$fish_pid" && set --erase -g ORCA_OMP_PREFILL; command test -z "$fish_pid" && unset ORCA_OMP_PREFILL; true`
|
||||
)
|
||||
expect(plan?.launchConfig.agentCommand).toBe('omp')
|
||||
})
|
||||
|
||||
it('returns null for oversized Windows flag drafts so callers paste after ready', () => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { withFreshOmpLaunch, isFreshOmpLaunchCommand } from './omp-fresh-launch'
|
||||
import { withOmpDraftCleanup } from './omp-draft-launch'
|
||||
import { isShellProcess } from './agent-detection'
|
||||
import type { SleepingAgentLaunchConfig } from './agent-session-resume'
|
||||
import {
|
||||
@@ -70,6 +72,8 @@ export function buildAgentStartupPlan(args: {
|
||||
if (!baseCommand.ok) {
|
||||
return null
|
||||
}
|
||||
const launchCommand =
|
||||
agent === 'omp' ? withFreshOmpLaunch(baseCommand.command, shell) : baseCommand.command
|
||||
const launchConfig = buildSleepingAgentLaunchConfig({
|
||||
...args,
|
||||
// Why: picker flags are a one-time launch choice; a resumed provider
|
||||
@@ -83,7 +87,7 @@ export function buildAgentStartupPlan(args: {
|
||||
}
|
||||
return {
|
||||
agent,
|
||||
launchCommand: baseCommand.command,
|
||||
launchCommand,
|
||||
expectedProcess: config.expectedProcess,
|
||||
followupPrompt: null,
|
||||
launchConfig,
|
||||
@@ -98,7 +102,10 @@ export function buildAgentStartupPlan(args: {
|
||||
const promptSeparator = config.argvPromptSeparator ? ` ${config.argvPromptSeparator}` : ''
|
||||
return {
|
||||
agent,
|
||||
launchCommand: `${baseCommand.command}${promptSeparator} ${quotedPrompt}`,
|
||||
launchCommand:
|
||||
agent === 'omp'
|
||||
? withFreshOmpLaunch(baseCommand.command, shell, `${promptSeparator} ${quotedPrompt}`)
|
||||
: `${launchCommand}${promptSeparator} ${quotedPrompt}`,
|
||||
expectedProcess: config.expectedProcess,
|
||||
followupPrompt: null,
|
||||
launchConfig,
|
||||
@@ -111,7 +118,7 @@ export function buildAgentStartupPlan(args: {
|
||||
if (config.promptInjectionMode === 'flag-prompt') {
|
||||
return {
|
||||
agent,
|
||||
launchCommand: `${baseCommand.command} --prompt ${quotedPrompt}`,
|
||||
launchCommand: `${launchCommand} --prompt ${quotedPrompt}`,
|
||||
expectedProcess: config.expectedProcess,
|
||||
followupPrompt: null,
|
||||
launchConfig,
|
||||
@@ -149,7 +156,7 @@ export function buildAgentStartupPlan(args: {
|
||||
if (config.promptInjectionMode === 'flag-prompt-interactive') {
|
||||
return {
|
||||
agent,
|
||||
launchCommand: `${baseCommand.command} --prompt-interactive ${quotedPrompt}`,
|
||||
launchCommand: `${launchCommand} --prompt-interactive ${quotedPrompt}`,
|
||||
expectedProcess: config.expectedProcess,
|
||||
followupPrompt: null,
|
||||
launchConfig,
|
||||
@@ -161,7 +168,7 @@ export function buildAgentStartupPlan(args: {
|
||||
if (config.promptInjectionMode === 'flag-interactive') {
|
||||
return {
|
||||
agent,
|
||||
launchCommand: `${baseCommand.command} -i ${quotedPrompt}`,
|
||||
launchCommand: `${launchCommand} -i ${quotedPrompt}`,
|
||||
expectedProcess: config.expectedProcess,
|
||||
followupPrompt: null,
|
||||
launchConfig,
|
||||
@@ -172,7 +179,7 @@ export function buildAgentStartupPlan(args: {
|
||||
|
||||
return {
|
||||
agent,
|
||||
launchCommand: baseCommand.command,
|
||||
launchCommand,
|
||||
expectedProcess: config.expectedProcess,
|
||||
followupPrompt: trimmedPrompt,
|
||||
launchConfig,
|
||||
@@ -222,6 +229,8 @@ export function buildAgentDraftLaunchPlan(args: {
|
||||
if (!baseCommand.ok) {
|
||||
return null
|
||||
}
|
||||
const launchCommand =
|
||||
agent === 'omp' ? withFreshOmpLaunch(baseCommand.command, shell) : baseCommand.command
|
||||
const launchConfig = buildSleepingAgentLaunchConfig({
|
||||
...args,
|
||||
// Why: see the new-session path above — resume must not replay picker flags.
|
||||
@@ -232,7 +241,7 @@ export function buildAgentDraftLaunchPlan(args: {
|
||||
const quoted = quoteStartupArg(trimmed, shell)
|
||||
plan = {
|
||||
agent,
|
||||
launchCommand: `${baseCommand.command} ${config.draftPromptFlag} ${quoted}`,
|
||||
launchCommand: `${launchCommand} ${config.draftPromptFlag} ${quoted}`,
|
||||
expectedProcess: config.expectedProcess,
|
||||
launchConfig,
|
||||
...appliedSessionOptionProps(baseCommand.appliedSessionOptions),
|
||||
@@ -244,7 +253,10 @@ export function buildAgentDraftLaunchPlan(args: {
|
||||
const clearVar = clearEnvCommand(config.draftPromptEnvVar, shell)
|
||||
plan = {
|
||||
agent,
|
||||
launchCommand: `${baseCommand.command}${commandSeparator(shell)}${clearVar}`,
|
||||
launchCommand:
|
||||
agent === 'omp' && isFreshOmpLaunchCommand(launchCommand)
|
||||
? withOmpDraftCleanup(launchCommand, shell)
|
||||
: `${launchCommand}${commandSeparator(shell)}${clearVar}`,
|
||||
expectedProcess: config.expectedProcess,
|
||||
launchConfig,
|
||||
...appliedSessionOptionProps(baseCommand.appliedSessionOptions),
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { mkdtempSync, writeFileSync, readFileSync, rmSync, mkdirSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import assert from 'node:assert/strict'
|
||||
import { runProcess } from '../../src/shared/child-process/run-process'
|
||||
import { withFreshOmpLaunch } from '../../src/shared/omp-fresh-launch'
|
||||
if (process.platform !== 'win32') {
|
||||
throw new Error('Run this smoke on a Windows host')
|
||||
}
|
||||
const root = mkdtempSync(join(tmpdir(), 'orca-fresh-windows-'))
|
||||
const config = join(root, 'fresh settings.yml')
|
||||
const capture = join(root, 'calls')
|
||||
const results = []
|
||||
try {
|
||||
writeFileSync(join(root, 'omp.cmd'), '@echo off\r\necho %*>>"%CAPTURE%"\r\nexit /b 17\r\n')
|
||||
for (const shell of ['cmd', 'powershell'] as const) {
|
||||
for (const state of ['present', 'missing', 'unset', 'directory']) {
|
||||
rmSync(config, { force: true, recursive: true })
|
||||
rmSync(capture, { force: true })
|
||||
if (state === 'present') {
|
||||
writeFileSync(config, 'autoResume: false\n')
|
||||
}
|
||||
if (state === 'directory') {
|
||||
mkdirSync(config)
|
||||
}
|
||||
const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
ORCA_BACKGROUND_LAUNCH: '1',
|
||||
ORCA_OMP_FRESH_CONFIG: config,
|
||||
CAPTURE: capture
|
||||
}
|
||||
if (state === 'unset') {
|
||||
delete env.ORCA_OMP_FRESH_CONFIG
|
||||
}
|
||||
const command = withFreshOmpLaunch('omp', shell, ' "task with spaces"')
|
||||
const runner = join(root, 'run.cmd')
|
||||
writeFileSync(runner, `@echo off\r\n${command}\r\n`)
|
||||
const result = await runProcess({
|
||||
program: shell === 'cmd' ? runner : 'powershell.exe',
|
||||
args:
|
||||
shell === 'cmd'
|
||||
? []
|
||||
: [
|
||||
'-NoProfile',
|
||||
'-NonInteractive',
|
||||
'-Command',
|
||||
`function omp { $args | ConvertTo-Json -Compress | Add-Content -LiteralPath $env:CAPTURE; $global:LASTEXITCODE = 17 }; ${
|
||||
command
|
||||
}; exit $global:LASTEXITCODE`
|
||||
],
|
||||
cwd: root,
|
||||
env
|
||||
})
|
||||
assert.equal(
|
||||
result.code,
|
||||
state === 'present' ? 17 : 1,
|
||||
JSON.stringify({ shell, state, ...result })
|
||||
)
|
||||
if (state === 'present') {
|
||||
const calls = readFileSync(capture, 'utf8').trim().split(/\r?\n/)
|
||||
assert.equal(calls.length, 1)
|
||||
assert.ok(calls[0].includes('--config'))
|
||||
assert.ok(calls[0].includes('task with spaces'))
|
||||
} else {
|
||||
assert.ok(
|
||||
result.stderr.includes('fresh OMP settings are unavailable'),
|
||||
JSON.stringify(result)
|
||||
)
|
||||
assert.throws(() => readFileSync(capture))
|
||||
}
|
||||
results.push({ shell, state, code: result.code, passed: true })
|
||||
}
|
||||
}
|
||||
console.log(JSON.stringify(results))
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Bun, with a read-only OMP source checkout as argv[2]. No model requests.
|
||||
import assert from 'node:assert/strict'
|
||||
import { mkdtemp, mkdir, writeFile, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { OMP_FRESH_CONFIG_SOURCE } from '../../src/shared/omp-fresh-launch.ts'
|
||||
|
||||
assert.ok(process.argv[2], 'Pass a read-only OMP checkout path')
|
||||
const scratch = await mkdtemp(join(tmpdir(), 'orca-omp-fresh-proof-'))
|
||||
process.env.HOME = join(scratch, 'home')
|
||||
process.env.USERPROFILE = process.env.HOME
|
||||
process.env.XDG_CONFIG_HOME = join(scratch, 'xdg-config')
|
||||
process.env.XDG_DATA_HOME = join(scratch, 'xdg-data')
|
||||
process.env.XDG_STATE_HOME = join(scratch, 'xdg-state')
|
||||
process.env.OMP_CODING_AGENT_DIR = join(scratch, 'agent')
|
||||
delete process.env.PI_CONFIG_FILES
|
||||
const source = (path) =>
|
||||
pathToFileURL(join(resolve(process.argv[2]), 'packages/coding-agent/src', path)).href
|
||||
const managers = []
|
||||
try {
|
||||
await mkdir(process.env.HOME, { recursive: true })
|
||||
const { SessionManager } = await import(source('session/session-manager.ts'))
|
||||
const { Settings, resetSettingsForTest } = await import(source('config/settings.ts'))
|
||||
const { createSessionManager } = await import(source('main.ts'))
|
||||
const cwd = join(scratch, 'project')
|
||||
await mkdir(cwd)
|
||||
const previous = SessionManager.create(cwd)
|
||||
managers.push(previous)
|
||||
previous.appendMessage({ role: 'user', content: 'previous task', timestamp: Date.now() })
|
||||
await previous.ensureOnDisk()
|
||||
await previous.flush()
|
||||
const config = join(scratch, 'fresh.yml')
|
||||
const userConfig = join(scratch, 'user.yml')
|
||||
await writeFile(config, OMP_FRESH_CONFIG_SOURCE)
|
||||
await writeFile(userConfig, 'autoResume: true\n')
|
||||
const settings = await Settings.init({ cwd, configFiles: [userConfig] })
|
||||
const resumed = await createSessionManager({}, cwd, settings)
|
||||
managers.push(resumed)
|
||||
assert.equal(resumed.getSessionId(), previous.getSessionId())
|
||||
resetSettingsForTest()
|
||||
const freshSettings = await Settings.init({ cwd, configFiles: [userConfig, config] })
|
||||
assert.equal(freshSettings.get('autoResume'), false)
|
||||
assert.equal(settings.get('autoResume'), true)
|
||||
const defaultSelection = await createSessionManager({}, cwd, freshSettings)
|
||||
assert.equal(defaultSelection, undefined, 'SDK creates a fresh session after undefined selection')
|
||||
const fresh = SessionManager.create(cwd)
|
||||
managers.push(fresh)
|
||||
assert.notEqual(fresh.getSessionId(), previous.getSessionId())
|
||||
assert.equal(fresh.getSessionDir(), previous.getSessionDir())
|
||||
const explicit = await createSessionManager(
|
||||
{ resume: previous.getSessionFile() },
|
||||
cwd,
|
||||
freshSettings
|
||||
)
|
||||
managers.push(explicit)
|
||||
assert.equal(explicit.getSessionId(), previous.getSessionId())
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
platform: process.platform,
|
||||
autoResumeReproduced: true,
|
||||
freshSelected: true,
|
||||
storageDirectoryPreserved: true,
|
||||
explicitResumePreserved: true,
|
||||
modelCalls: 0,
|
||||
scope:
|
||||
'Actual OMP Settings overlays, persistent SessionManager and createSessionManager; no rendered UI'
|
||||
})
|
||||
)
|
||||
} finally {
|
||||
for (const manager of managers) {
|
||||
await manager?.close()
|
||||
}
|
||||
await rm(scratch, { recursive: true, force: true })
|
||||
}
|
||||
Reference in New Issue
Block a user