mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 18:37:24 +00:00
perf(terminal): prove an idle Linux daemon shell from /proc so Enter skips the paste settle (#151)
terminal.send text+Enter to a daemon-backed shell always waited the 500 ms agent paste settle: the daemon's shell-foreground answer is lifecycle ownership, which is only proven after a recovery or full-screen exit, never for a fresh or plain-command shell. On Linux the idle-prompt claim now also accepts the shell's own /proc stat (process group is the terminal foreground group, i.e. ps '+'), scoped to the target PTY; other hosts unchanged. The acceptance wake split uses one persistent paired connection per wake (the desktop's transport) by default; transport=cli keeps the per-call socket the CLI dials.
This commit is contained in:
@@ -7,17 +7,46 @@
|
||||
// termCreate terminal.create (PTY + login shell)
|
||||
// echo typed command -> echo seen (terminal.send / terminal.read polling)
|
||||
// total request sent -> echo seen ("wake delivery")
|
||||
// Uses the CLI's RuntimeClient (one client per wake, like a reconnecting desktop). Prints one JSON line.
|
||||
// <repo>/node_modules/.bin/tsx cloud/apps/acceptance/desktop/wake-split.ts [n=10] [machineSize=standard]
|
||||
// transport=persistent (default): one paired connection per wake, reused for every call, as the
|
||||
// desktop caches per environment (firstList carries the handshake). transport=cli: the CLI's
|
||||
// RuntimeClient, which dials a fresh socket + E2EE handshake for every call (~1 RTT more each).
|
||||
// Prints one JSON line.
|
||||
// <repo>/node_modules/.bin/tsx cloud/apps/acceptance/desktop/wake-split.ts [n=10] [machineSize=standard] [persistent|cli]
|
||||
import { mkdtempSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { RuntimeClient } from '../../../../src/cli/runtime/client'
|
||||
import { ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES } from '../../../../src/shared/electron-remote-runtime-client-capabilities'
|
||||
import { pairedRuntimeConnectionFromOffer } from '../../../../src/shared/remote-runtime/pairing/paired-runtime-connection'
|
||||
import { parsePairingCode } from '../../../../src/shared/remote-runtime/pairing/pairing'
|
||||
import { RemoteRuntimeRequestConnection } from '../../../../src/shared/remote-runtime/requests/remote-runtime-request-connection'
|
||||
import { control, vercel } from '../src/clients'
|
||||
import { orca } from '../src/orca-cli'
|
||||
|
||||
const n = Number(process.argv[2] ?? 10)
|
||||
const machineSize = (process.argv[3] ?? 'standard') as 'standard' | 'large'
|
||||
const transport = process.argv[4] === 'cli' ? 'cli' : 'persistent'
|
||||
type Caller = { call: <T>(method: string, params: unknown) => Promise<{ result: T }>; close: () => void }
|
||||
function connect(pairingCode: string): Caller {
|
||||
if (transport === 'cli') {
|
||||
const client = new RuntimeClient(mkdtempSync(join(tmpdir(), 'orca-wake-')), 60_000, pairingCode, null)
|
||||
return { call: (method, params) => client.call(method, params), close: () => {} }
|
||||
}
|
||||
const connection = new RemoteRuntimeRequestConnection(
|
||||
pairedRuntimeConnectionFromOffer(parsePairingCode(pairingCode)!),
|
||||
ELECTRON_REMOTE_RUNTIME_CLIENT_CAPABILITIES
|
||||
)
|
||||
return {
|
||||
call: async <T,>(method: string, params: unknown) => {
|
||||
const response = (await connection.request(method, params, 60_000)) as { ok: boolean; result?: T; error?: { message?: string } }
|
||||
if (!response.ok) {
|
||||
throw new Error(`${method}: ${response.error?.message ?? 'failed'}`)
|
||||
}
|
||||
return { result: response.result as T }
|
||||
},
|
||||
close: () => connection.close()
|
||||
}
|
||||
}
|
||||
const who = { sub: 'w9-wake-split' }
|
||||
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
||||
const out: Record<string, number[]> = { 'resume.vm': [], 'orcad.ready': [], firstList: [], termCreate: [], echo: [], total: [], api: [] }
|
||||
@@ -52,7 +81,7 @@ try {
|
||||
errors.push(`resume ${w.status} ${w.body?.reason ?? ''}`)
|
||||
continue
|
||||
}
|
||||
const client = new RuntimeClient(mkdtempSync(join(tmpdir(), 'orca-wake-')), 60_000, w.body.connection.pairingCode, null)
|
||||
const client = connect(w.body.connection.pairingCode)
|
||||
await client.call('terminal.list', { worktree })
|
||||
const t2 = Date.now()
|
||||
const term = await client.call<{ terminal: { handle: string } }>('terminal.create', { worktree, title: `wake-${i}` })
|
||||
@@ -69,6 +98,7 @@ try {
|
||||
}
|
||||
}
|
||||
const t3 = Date.now()
|
||||
client.close()
|
||||
if (!seen) {
|
||||
errors.push(`echo not seen (wake ${i})`)
|
||||
continue
|
||||
@@ -96,5 +126,5 @@ try {
|
||||
} finally {
|
||||
await control.as(who, 'DELETE', `/v1/sandboxes/${id}`)
|
||||
}
|
||||
console.log(JSON.stringify({ sandboxId: id, machineSize, samples: out, errors }))
|
||||
console.log(JSON.stringify({ sandboxId: id, machineSize, transport, samples: out, errors }))
|
||||
process.exit(0)
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { confirmIdleShellForeground, linuxShellOwnsForeground } from './idle-shell-prompt-claim'
|
||||
|
||||
const FOREGROUND_BASH = '2145 (bash) S 1840 2145 2145 34816 2145 4194304 1 0 0'
|
||||
|
||||
function setPlatform(platform: NodeJS.Platform) {
|
||||
vi.spyOn(process, 'platform', 'get').mockReturnValue(platform)
|
||||
}
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
describe('linuxShellOwnsForeground', () => {
|
||||
it('accepts a shell whose process group is the terminal foreground group', () => {
|
||||
expect(linuxShellOwnsForeground(FOREGROUND_BASH)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects a shell while a command owns the terminal foreground', () => {
|
||||
expect(linuxShellOwnsForeground('2145 (bash) S 1840 2145 2145 34816 2201 4194304')).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects a non-shell root, a stopped shell, and a process without a terminal', () => {
|
||||
expect(linuxShellOwnsForeground('2145 (node) S 1840 2145 2145 34816 2145 0')).toBe(false)
|
||||
expect(linuxShellOwnsForeground('2145 (bash) T 1840 2145 2145 34816 2145 0')).toBe(false)
|
||||
expect(linuxShellOwnsForeground('2145 (bash) S 1 2145 2145 0 -1 0')).toBe(false)
|
||||
})
|
||||
|
||||
it('parses past a comm containing spaces and parentheses', () => {
|
||||
expect(linuxShellOwnsForeground('7 (a) b) S 1 7 7 34816 7 0')).toBe(false)
|
||||
expect(linuxShellOwnsForeground('garbage')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('confirmIdleShellForeground', () => {
|
||||
it('keeps the provider proof without reading the process', async () => {
|
||||
setPlatform('linux')
|
||||
const listProcesses = vi.fn()
|
||||
const readStat = vi.fn()
|
||||
const controller = { confirmShellForeground: async () => true, listProcesses }
|
||||
await expect(confirmIdleShellForeground(controller, 'pty-1', readStat)).resolves.toBe(true)
|
||||
expect(listProcesses).not.toHaveBeenCalled()
|
||||
expect(readStat).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('proves an unowned Linux shell from its own stat, scoped to the target PTY', async () => {
|
||||
setPlatform('linux')
|
||||
const listProcesses = vi.fn(async () => [
|
||||
{ id: 'pty-1', cwd: '/', title: '', rootProcessId: 2145 }
|
||||
])
|
||||
const readStat = vi.fn(async () => FOREGROUND_BASH)
|
||||
const controller = { confirmShellForeground: async () => false, listProcesses }
|
||||
await expect(confirmIdleShellForeground(controller, 'pty-1', readStat)).resolves.toBe(true)
|
||||
expect(listProcesses).toHaveBeenCalledWith('local', {
|
||||
ptyIds: ['pty-1'],
|
||||
targetOwnerOnly: true
|
||||
})
|
||||
expect(readStat).toHaveBeenCalledWith(2145)
|
||||
})
|
||||
|
||||
it('stays unproven when the PTY has no live root process or the read fails', async () => {
|
||||
setPlatform('linux')
|
||||
const missing = { confirmShellForeground: async () => false, listProcesses: async () => [] }
|
||||
await expect(
|
||||
confirmIdleShellForeground(missing, 'pty-1', async () => FOREGROUND_BASH)
|
||||
).resolves.toBe(false)
|
||||
const failing = {
|
||||
confirmShellForeground: async () => false,
|
||||
listProcesses: async () => [{ id: 'pty-1', cwd: '/', title: '', rootProcessId: 2145 }]
|
||||
}
|
||||
const unreadable = async () => {
|
||||
throw new Error('ENOENT')
|
||||
}
|
||||
await expect(confirmIdleShellForeground(failing, 'pty-1', unreadable)).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it('leaves other hosts on the provider answer', async () => {
|
||||
setPlatform('darwin')
|
||||
const listProcesses = vi.fn()
|
||||
const controller = { confirmShellForeground: async () => false, listProcesses }
|
||||
await expect(
|
||||
confirmIdleShellForeground(controller, 'pty-1', async () => FOREGROUND_BASH)
|
||||
).resolves.toBe(false)
|
||||
expect(listProcesses).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,3 +1,8 @@
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../../shared/execution-host'
|
||||
import { isShellProcess } from '../../../../shared/agent-status/process/shell-process-detection'
|
||||
import type { RuntimePtyController } from '../lifecycle/runtime-pty-controller-contract'
|
||||
|
||||
// A hung host proof must not outlast the settle it would have saved.
|
||||
const SHELL_FOREGROUND_PROOF_DEADLINE_MS = 250
|
||||
|
||||
@@ -30,3 +35,70 @@ export async function claimIdleShellPrompt(evidence: IdleShellPromptEvidence): P
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
||||
type ShellForegroundController = Pick<
|
||||
RuntimePtyController,
|
||||
'confirmShellForeground' | 'listProcesses'
|
||||
>
|
||||
|
||||
/** The host proof for one PTY, or null when its controller cannot prove a shell foreground at all. */
|
||||
export function idleShellForegroundProof(
|
||||
controller: ShellForegroundController | null | undefined,
|
||||
ptyId: string
|
||||
): (() => Promise<boolean>) | null {
|
||||
return controller?.confirmShellForeground
|
||||
? () => confirmIdleShellForeground(controller, ptyId)
|
||||
: null
|
||||
}
|
||||
|
||||
/** Daemon PTYs prove lifecycle ownership only after a full-screen exit, never for a fresh or
|
||||
* plain-command shell, so every Enter paid the paste settle. Linux reads the shell's own /proc stat
|
||||
* for that proof (no ps); other hosts keep the provider's answer. */
|
||||
export async function confirmIdleShellForeground(
|
||||
controller: ShellForegroundController,
|
||||
ptyId: string,
|
||||
readStat: (pid: number) => Promise<string> = readProcStat
|
||||
): Promise<boolean> {
|
||||
if (await controller.confirmShellForeground?.(ptyId)) {
|
||||
return true
|
||||
}
|
||||
if (process.platform !== 'linux' || !controller.listProcesses) {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
const rows = await controller.listProcesses(LOCAL_EXECUTION_HOST_ID, {
|
||||
ptyIds: [ptyId],
|
||||
targetOwnerOnly: true
|
||||
})
|
||||
const pid = rows.find((row) => row.id === ptyId)?.rootProcessId
|
||||
return pid ? linuxShellOwnsForeground(await readStat(pid)) : false
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function readProcStat(pid: number): Promise<string> {
|
||||
return readFile(`/proc/${pid}/stat`, 'utf8')
|
||||
}
|
||||
|
||||
/** `pid (comm) state ppid pgrp session tty_nr tpgid ...`: a shell whose process group is its
|
||||
* terminal's foreground group, which is what ps marks with `+`. comm may contain spaces or ')'. */
|
||||
export function linuxShellOwnsForeground(stat: string): boolean {
|
||||
const open = stat.indexOf('(')
|
||||
const close = stat.lastIndexOf(')')
|
||||
if (open === -1 || close < open) {
|
||||
return false
|
||||
}
|
||||
if (!isShellProcess(stat.slice(open + 1, close))) {
|
||||
return false
|
||||
}
|
||||
const [state, , pgrp, , ttyNr, tpgid] = stat
|
||||
.slice(close + 1)
|
||||
.trim()
|
||||
.split(/\s+/)
|
||||
if (!state || state === 'T' || state === 't' || state === 'Z' || state === 'X') {
|
||||
return false
|
||||
}
|
||||
const group = Number(pgrp)
|
||||
return Number(ttyNr) !== 0 && group > 0 && Number(tpgid) === group
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from '../../orchestration/addressing/orchestration-fleet-agent-status-snapshot'
|
||||
import { resolveStructuredWorkerAuthority } from '../../orchestration/terminal-authority/structured-sessions/structured-worker-authority'
|
||||
import { matchesProcessIncarnation } from '../../orchestration/terminal-authority/release/worker-terminal-process-liveness'
|
||||
import { claimIdleShellPrompt } from '../input/idle-shell-prompt-claim'
|
||||
import { claimIdleShellPrompt, idleShellForegroundProof } from '../input/idle-shell-prompt-claim'
|
||||
|
||||
export abstract class OrcaRuntimeWithGetOrchestrationDispatchAuthority extends OrcaRuntimeWithVerifyOrchestrationCompatibilityCaller {
|
||||
protected abstract issueHandle(
|
||||
@@ -83,7 +83,7 @@ export abstract class OrcaRuntimeWithGetOrchestrationDispatchAuthority extends O
|
||||
}
|
||||
|
||||
protected claimIdleShellPromptForPty(ptyId: string): Promise<boolean> {
|
||||
const confirm = this.ptyController?.confirmShellForeground?.bind(this.ptyController)
|
||||
const confirm = idleShellForegroundProof(this.ptyController, ptyId)
|
||||
const readPane = this.getAgentStatusSnapshotForPaneFn
|
||||
return claimIdleShellPrompt({
|
||||
hasPaneAgent: () => {
|
||||
@@ -98,7 +98,7 @@ export abstract class OrcaRuntimeWithGetOrchestrationDispatchAuthority extends O
|
||||
},
|
||||
claimPromptMarker: () =>
|
||||
this.headlessTerminals.get(ptyId)?.ownership.claimIdlePrompt() === true,
|
||||
confirmShellForeground: confirm ? () => confirm(ptyId) : null
|
||||
confirmShellForeground: confirm
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user