mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
Merge remote-tracking branch 'origin/brennanb2025/structured-remove-chat-button' into brennanb2025/structured-tui-adoption
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
import { PassThrough } from 'node:stream'
|
||||
import type { spawn } from 'node:child_process'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { spawnProcess } from '../../shared/child-process/run-process'
|
||||
import {
|
||||
isCodexAppServerRequestError,
|
||||
openCodexAppServerConnection,
|
||||
@@ -76,7 +76,7 @@ type StubChild = EventEmitter & {
|
||||
/** Full control over framing and death, which a real child cannot give. */
|
||||
function stubChild(options: { exitOnStdinEnd?: boolean } = {}): {
|
||||
child: StubChild
|
||||
spawnImpl: typeof spawn
|
||||
spawnImpl: typeof spawnProcess
|
||||
written: Record<string, unknown>[]
|
||||
} {
|
||||
const child = new EventEmitter() as StubChild
|
||||
@@ -96,7 +96,7 @@ function stubChild(options: { exitOnStdinEnd?: boolean } = {}): {
|
||||
if (options.exitOnStdinEnd !== false) {
|
||||
child.stdin.on('finish', () => child.emit('exit', 0, null))
|
||||
}
|
||||
return { child, spawnImpl: (() => child) as unknown as typeof spawn, written }
|
||||
return { child, spawnImpl: (() => child) as unknown as typeof spawnProcess, written }
|
||||
}
|
||||
|
||||
/** Answers the handshake so `openCodexAppServerConnection` can resolve. */
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process'
|
||||
import { spawnProcess } from '../../shared/child-process/run-process'
|
||||
import { buildCodexAppServerExitError } from './codex-app-server-exit-error'
|
||||
import { isAppServerRecord, parseCodexAppServerJsonLine } from './codex-app-server-jsonl'
|
||||
import { waitForProcessExitUntil } from './codex-process-exit-deadline'
|
||||
@@ -70,17 +70,18 @@ type PendingRequest = {
|
||||
export async function openCodexAppServerConnection(
|
||||
launch: CodexAppServerLaunch,
|
||||
handlers: CodexAppServerConnectionHandlers = {},
|
||||
spawnImpl: typeof spawn = spawn
|
||||
spawnImpl: typeof spawnProcess = spawnProcess
|
||||
): Promise<CodexAppServerConnection> {
|
||||
const childEnv: NodeJS.ProcessEnv = { ...process.env, ...launch.env }
|
||||
for (const key of launch.envToDelete ?? []) {
|
||||
delete childEnv[key]
|
||||
}
|
||||
const child = spawnImpl(launch.command, launch.args, {
|
||||
const child = spawnImpl({
|
||||
program: launch.command,
|
||||
args: launch.args,
|
||||
env: childEnv,
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
windowsHide: true
|
||||
}) as ChildProcessWithoutNullStreams
|
||||
cwd: process.cwd()
|
||||
})
|
||||
|
||||
const pending = new Map<number, PendingRequest>()
|
||||
let stderrTail = ''
|
||||
|
||||
@@ -6,12 +6,13 @@
|
||||
// workspace that is not a repository at all. Keying by id rather than by path
|
||||
// makes a worktree, a folder workspace, a WSL distro, and an SSH host identical.
|
||||
//
|
||||
// `app` is imported lazily so the store stays usable from tests and from the
|
||||
// headless/SSH runtime, which have no Electron app object.
|
||||
// The host environment port supplies the root so desktop Electron and the
|
||||
// headless/SSH runtime resolve their own durable state directories.
|
||||
|
||||
import { createHash } from 'node:crypto'
|
||||
import { join } from 'node:path'
|
||||
import type { AgentSessionJournalIdentity } from '../../../shared/agent-session-journal-types'
|
||||
import { getAppEnvironment } from '../../../shared/app-environment'
|
||||
|
||||
const JOURNAL_DIR_NAME = 'agent-session-journal'
|
||||
|
||||
@@ -35,9 +36,7 @@ export function journalDirectoryFor(
|
||||
)
|
||||
}
|
||||
|
||||
/** Default host state root. Electron is required lazily so importing this
|
||||
* module from a non-Electron runtime does not pull the whole app in. */
|
||||
export async function defaultJournalRoot(): Promise<string> {
|
||||
const { app } = await import('electron')
|
||||
return app.getPath('userData')
|
||||
/** Default host state root. */
|
||||
export function defaultJournalRoot(): Promise<string> {
|
||||
return Promise.resolve(getAppEnvironment().getPath('userData'))
|
||||
}
|
||||
|
||||
@@ -8,16 +8,13 @@
|
||||
* closed to manual recovery rather than minting a second writer.
|
||||
*/
|
||||
|
||||
import { execFile } from 'node:child_process'
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { promisify } from 'node:util'
|
||||
import type {
|
||||
AgentSessionIdentityMatchField,
|
||||
AgentSessionOwnerProbe
|
||||
} from '../../shared/agent-session-lease-adjudication'
|
||||
import type { AgentSessionProcessIdentity } from '../../shared/agent-session-record'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
import { runProcess } from '../../shared/child-process/run-process'
|
||||
|
||||
/** Start times drift by scheduler granularity and clock reads; compare with a tolerance. */
|
||||
export const PROCESS_START_TIME_TOLERANCE_MS = 2_000
|
||||
@@ -64,10 +61,15 @@ async function readLinuxProcessStartTimeMs(pid: number): Promise<number | null>
|
||||
|
||||
async function readDarwinProcessStartTimeMs(pid: number): Promise<number | null> {
|
||||
try {
|
||||
const { stdout } = await execFileAsync('ps', ['-o', 'lstart=', '-p', String(pid)], {
|
||||
timeout: PROCESS_START_TIME_TIMEOUT_MS
|
||||
const result = await runProcess({
|
||||
program: 'ps',
|
||||
args: ['-o', 'lstart=', '-p', String(pid)],
|
||||
timeoutMs: PROCESS_START_TIME_TIMEOUT_MS
|
||||
})
|
||||
const parsed = Date.parse(stdout.trim())
|
||||
if (result.timedOut || result.code !== 0) {
|
||||
return null
|
||||
}
|
||||
const parsed = Date.parse(result.stdout.trim())
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
} catch {
|
||||
return null
|
||||
@@ -79,12 +81,15 @@ async function readWindowsProcessStartTimeMs(pid: number): Promise<number | null
|
||||
const script =
|
||||
`$p = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; ` +
|
||||
'if ($p) { $p.CreationDate.ToUniversalTime().ToString("o") }'
|
||||
const { stdout } = await execFileAsync(
|
||||
'powershell.exe',
|
||||
['-NoProfile', '-NonInteractive', '-Command', script],
|
||||
{ timeout: PROCESS_START_TIME_TIMEOUT_MS, windowsHide: true }
|
||||
)
|
||||
const parsed = Date.parse(stdout.trim())
|
||||
const result = await runProcess({
|
||||
program: 'powershell.exe',
|
||||
args: ['-NoProfile', '-NonInteractive', '-Command', script],
|
||||
timeoutMs: PROCESS_START_TIME_TIMEOUT_MS
|
||||
})
|
||||
if (result.timedOut || result.code !== 0) {
|
||||
return null
|
||||
}
|
||||
const parsed = Date.parse(result.stdout.trim())
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
} catch {
|
||||
return null
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { spawn } from 'node:child_process'
|
||||
import { win32 as pathWin32 } from 'node:path'
|
||||
import { spawnProcess } from '../../shared/child-process/run-process'
|
||||
import { resolveWindowsShellStartupFamily } from '../../shared/windows-terminal-shell'
|
||||
import {
|
||||
resolveProfileLoadingFallbackShell,
|
||||
@@ -86,12 +86,7 @@ function spawnShellAndReadEnvironment(shell: string): Promise<NodeJS.ProcessEnv
|
||||
return new Promise((resolve) => {
|
||||
let settled = false
|
||||
const chunks: Buffer[] = []
|
||||
const child = spawn(shell, args, {
|
||||
env: process.env,
|
||||
stdio: ['ignore', 'pipe', 'ignore'],
|
||||
detached: false,
|
||||
windowsHide: true
|
||||
})
|
||||
const child = spawnProcess({ program: shell, args, env: process.env })
|
||||
const finish = (value: NodeJS.ProcessEnv | null): void => {
|
||||
if (settled) {
|
||||
return
|
||||
@@ -108,6 +103,11 @@ function spawnShellAndReadEnvironment(shell: string): Promise<NodeJS.ProcessEnv
|
||||
}
|
||||
finish(null)
|
||||
}, SPAWN_TIMEOUT_MS)
|
||||
child.stdin.on('error', () => {})
|
||||
child.stdout.on('error', () => finish(null))
|
||||
child.stderr.on('error', () => {})
|
||||
child.stdin.end()
|
||||
child.stderr.resume()
|
||||
child.stdout.on('data', (chunk: Buffer) => chunks.push(chunk))
|
||||
child.on('error', () => finish(null))
|
||||
child.on('close', () => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
spawn as nodeSpawn,
|
||||
spawnSync as nodeSpawnSync,
|
||||
type ChildProcess,
|
||||
type ChildProcessWithoutNullStreams,
|
||||
type SpawnOptions as NodeSpawnOptions
|
||||
} from 'node:child_process'
|
||||
import { buildWindowsCmdShimCommandLine, isCmdInterpretedProgram } from './windows-command-line'
|
||||
@@ -116,9 +117,13 @@ export function resolveSpawn(spec: ProcessSpec, platform: NodeJS.Platform): Reso
|
||||
* `runProcess` handles that for you; here it cannot, because a blanket handler
|
||||
* would also defeat callers that track and remove their own listeners.
|
||||
*/
|
||||
export function spawnProcess(spec: ProcessSpec): ChildProcess {
|
||||
export function spawnProcess(spec: ProcessSpec): ChildProcessWithoutNullStreams {
|
||||
const resolved = resolveSpawn(spec, process.platform)
|
||||
return nodeSpawn(resolved.file, [...resolved.args], resolved.options)
|
||||
return nodeSpawn(
|
||||
resolved.file,
|
||||
[...resolved.args],
|
||||
resolved.options
|
||||
) as ChildProcessWithoutNullStreams
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user