From 893be16f77af6568cc0bbba2ceaf4a2f0a775084 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 9 Nov 2026 19:38:55 -0800 Subject: [PATCH] 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. --- cloud/apps/acceptance/desktop/wake-split.ts | 38 ++++++++- .../input/idle-shell-prompt-claim.test.ts | 84 +++++++++++++++++++ .../terminal/input/idle-shell-prompt-claim.ts | 72 ++++++++++++++++ .../terminal-orchestration-authority.ts | 6 +- 4 files changed, 193 insertions(+), 7 deletions(-) create mode 100644 src/main/runtime/terminal/input/idle-shell-prompt-claim.test.ts diff --git a/cloud/apps/acceptance/desktop/wake-split.ts b/cloud/apps/acceptance/desktop/wake-split.ts index da746442ff0..b5f79c73566 100644 --- a/cloud/apps/acceptance/desktop/wake-split.ts +++ b/cloud/apps/acceptance/desktop/wake-split.ts @@ -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. -// /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. +// /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: (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 (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 = { '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) diff --git a/src/main/runtime/terminal/input/idle-shell-prompt-claim.test.ts b/src/main/runtime/terminal/input/idle-shell-prompt-claim.test.ts new file mode 100644 index 00000000000..4e3f6a190dd --- /dev/null +++ b/src/main/runtime/terminal/input/idle-shell-prompt-claim.test.ts @@ -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() + }) +}) diff --git a/src/main/runtime/terminal/input/idle-shell-prompt-claim.ts b/src/main/runtime/terminal/input/idle-shell-prompt-claim.ts index c5aac370415..b62c26c13b9 100644 --- a/src/main/runtime/terminal/input/idle-shell-prompt-claim.ts +++ b/src/main/runtime/terminal/input/idle-shell-prompt-claim.ts @@ -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) | 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 = readProcStat +): Promise { + 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 { + 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 +} diff --git a/src/main/runtime/terminal/output/terminal-orchestration-authority.ts b/src/main/runtime/terminal/output/terminal-orchestration-authority.ts index 7c350e70396..f63c392532b 100644 --- a/src/main/runtime/terminal/output/terminal-orchestration-authority.ts +++ b/src/main/runtime/terminal/output/terminal-orchestration-authority.ts @@ -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 { - 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 }) }