From 4523a4b3c4ed57fc7d5bedf9e74ed397f219ea46 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 2 Oct 2026 07:54:35 -0700 Subject: [PATCH] perf(agent-launch): read a local pane's foreground from its own terminal, not the whole process table The foreground read that gates every launch paste ran the daemon's inspectProcess capture and then a fresh scan, each a whole-machine ps; the fresh one also waits for any capture already running before it starts its own. Measured here at load 5: 1.2 s a read (M4Air QA: 3.4-5.0 s, and worker starts 17.6-32 s against main's 9-12 s at load 25-84). On a local macOS or Linux host, take the pane's root pid from the provider's session inventory and run one ps limited to that pane's terminal. Its foreground process group decides: the launched agent or any non-shell member is the agent (a wrapper that did not exec its agent leads the group), a group of shells alone is the shell. Same pane, same verdict: 2.7 ms a read. SSH hosts keep the relay's observation and name. --- .../agent-transcript-pane-test-harness.ts | 14 ++ ...ed-agent-composer-readiness-claude.test.ts | 210 ++++++++---------- ...nched-agent-crash-guard.live-shell.test.ts | 1 + .../launched-agent-exit-delivery.test.ts | 46 +++- .../runtime/launched-agent-foreground.test.ts | 57 ++++- src/main/runtime/launched-agent-foreground.ts | 36 +-- .../runtime/terminal-foreground-group.test.ts | 71 ++++++ src/main/runtime/terminal-foreground-group.ts | 103 +++++++++ 8 files changed, 393 insertions(+), 145 deletions(-) create mode 100644 src/main/runtime/terminal-foreground-group.test.ts create mode 100644 src/main/runtime/terminal-foreground-group.ts diff --git a/src/main/runtime/agent-transcript-pane-test-harness.ts b/src/main/runtime/agent-transcript-pane-test-harness.ts index c60459adcf9..89899702c7c 100644 --- a/src/main/runtime/agent-transcript-pane-test-harness.ts +++ b/src/main/runtime/agent-transcript-pane-test-harness.ts @@ -31,6 +31,8 @@ export type TranscriptPaneOptions = { onProcessInspection?: () => void /** What the host's shell-foreground check answers: the spawned shell holds the foreground. */ shellForegroundProven?: boolean + /** The pane's root process the provider reports; absent for a provider without an inventory. */ + paneRootPid?: number onShellForegroundProof?: () => void } @@ -81,6 +83,18 @@ export async function createTranscriptPane( } } : {}), + ...(options.paneRootPid !== undefined + ? { + listProcesses: async () => [ + { + id: TRANSCRIPT_PANE_PTY_ID, + rootProcessId: options.paneRootPid, + cwd: '/repo/app', + title: 'Terminal' + } + ] + } + : {}), ...(options.shellForegroundProven !== undefined ? { confirmShellForeground: async () => { diff --git a/src/main/runtime/launched-agent-composer-readiness-claude.test.ts b/src/main/runtime/launched-agent-composer-readiness-claude.test.ts index f87564c2156..60557680044 100644 --- a/src/main/runtime/launched-agent-composer-readiness-claude.test.ts +++ b/src/main/runtime/launched-agent-composer-readiness-claude.test.ts @@ -17,6 +17,17 @@ import { } from './launched-agent-composer-readiness' import { resolveRemoteForegroundEvidence } from '../providers/agent-foreground-process' import type { ProcessTableRow } from '../../shared/process-table-snapshot' +import type * as TerminalForegroundGroup from './terminal-foreground-group' + +// What `ps` limited to the pane's terminal answers; the verdict over it stays the real one. +const paneTerminal = vi.hoisted(() => { + const state: { rows: ProcessTableRow[] | null } = { rows: null } + return state +}) +vi.mock('./terminal-foreground-group', async (importOriginal) => ({ + ...(await importOriginal()), + readTerminalProcessRows: vi.fn(async () => paneTerminal.rows) +})) vi.mock('electron', () => ({ BrowserWindow: { fromId: vi.fn(() => null) }, @@ -140,47 +151,78 @@ describe('what holds a launched agent’s terminal', () => { Object.defineProperty(process, 'platform', originalPlatform) }) - describe('macOS and Linux: one fresh foreground read decides', () => { - beforeEach(() => setPlatform('darwin')) - - it.each([ - // For its first 5 s the daemon's cached read names the launch agent while zsh is in front. - ['claude', 'zsh', 'shell'], - // A stub agent that crashed: the cached read can still name it after zsh took the terminal. - ['python3', 'zsh', 'shell'], - ['claude', 'claude', 'agent'], - // A native Claude names itself by its version. - ['zsh', '2.1.285', 'agent'], - ['zsh', 'node', 'agent'], - ['-zsh', '-zsh', 'shell'], - ['bash', 'bash', 'shell'], - [null, 'zsh', 'shell'], - ['claude', null, 'unknown'] - ] as const)('cached %s, scan %s: %s', async (foregroundProcess, scanned, found) => { - const scan = vi.fn() - const proof = vi.fn() - const { runtime } = await createTranscriptPane({ - paneTitle: 'Claude Code', - foregroundProcess, - confirmedForegroundProcess: scanned, - onForegroundScan: scan, - shellForegroundProven: false, - onShellForegroundProof: proof, - launchAgent: 'claude', - data: '' - }) - - await expect( - runtime.readLaunchedAgentForeground(TRANSCRIPT_PANE_PTY_ID, 'claude') - ).resolves.toBe(found) - expect(scan).toHaveBeenCalledOnce() - expect(proof).not.toHaveBeenCalled() + describe('macOS and Linux: the pane terminal’s own foreground group decides', () => { + beforeEach(() => { + setPlatform('darwin') + paneTerminal.rows = null }) - it('proves nothing on a controller without a fresh scan', async () => { + // Captured with `ps -o pid=,ppid=,pgid=,tpgid=,stat=,command=` on a pane spawned the way a macOS + // pane is, under `login`: zsh holds the terminal in its own group, never the root's. + const login = (tpgid: number): ProcessTableRow => ({ + pid: 60404, + ppid: 60394, + pgid: 60404, + tpgid, + stat: 'Ss', + command: '/usr/bin/login -flpq user /bin/bash --noprofile --norc -p -c' + }) + const zshAt = (tpgid: number): ProcessTableRow => ({ + pid: 60406, + ppid: 60404, + pgid: 60406, + tpgid, + stat: tpgid === 60406 ? 'S+' : 'S', + command: '-/bin/zsh -f' + }) + + it.each([ + // A crashed stub's name can outlive it in the cached read; the rows show zsh back in front. + ['zsh back at its prompt', [login(60406), zshAt(60406)], 'shell'], + [ + 'claude in front', + [ + login(60500), + zshAt(60500), + { pid: 60500, ppid: 60406, pgid: 60500, tpgid: 60500, stat: 'S+', command: 'claude' } + ], + 'agent' + ] + ] as const)( + '%s: answers from the rows, never the cached name or a whole-machine scan', + async (_label, rows, found) => { + paneTerminal.rows = [...rows] + const scan = vi.fn() + const inspection = vi.fn() + const { runtime } = await createTranscriptPane({ + paneTitle: 'Claude Code', + foregroundProcess: 'python3', + confirmedForegroundProcess: 'claude', + onForegroundScan: scan, + processInspection: { foregroundProcess: 'claude', hasChildProcesses: true }, + onProcessInspection: inspection, + paneRootPid: 60404, + launchAgent: 'claude', + data: '' + }) + + await expect( + runtime.readLaunchedAgentForeground(TRANSCRIPT_PANE_PTY_ID, 'claude') + ).resolves.toBe(found) + expect(scan).not.toHaveBeenCalled() + expect(inspection).not.toHaveBeenCalled() + } + ) + + it.each([ + ['without the pane’s root process', undefined, [login(60406), zshAt(60406)]], + ['when ps cannot read the pane’s terminal', 60404, null] + ] as const)('proves nothing %s', async (_label, paneRootPid, rows) => { + paneTerminal.rows = rows ? [...rows] : null const { runtime } = await createTranscriptPane({ paneTitle: 'Claude Code', foregroundProcess: 'claude', + ...(paneRootPid ? { paneRootPid } : {}), launchAgent: 'claude', data: '' }) @@ -189,85 +231,13 @@ describe('what holds a launched agent’s terminal', () => { runtime.readLaunchedAgentForeground(TRANSCRIPT_PANE_PTY_ID, 'claude') ).resolves.toBe('unknown') }) + }) - // Captured with `ps -o pid=,ppid=,pgid=,tpgid=,stat=,command=` on a pane spawned the way a macOS - // pane is, under `login`: zsh holds the terminal in its own group, never the root's. - it('reads a login-wrapped pane’s zsh at its prompt as the shell', async () => { - const rows: ProcessTableRow[] = [ - { - pid: 60404, - ppid: 60394, - pgid: 60404, - tpgid: 60406, - stat: 'Ss', - tty: 'ttys004', - startTime: 'Fri Oct 2 01:20:11 2026', - command: '/usr/bin/login -flpq user /bin/bash --noprofile --norc -p -c' - }, - { - pid: 60406, - ppid: 60404, - pgid: 60406, - tpgid: 60406, - stat: 'S+', - tty: 'ttys004', - startTime: 'Fri Oct 2 01:20:11 2026', - command: '-/bin/zsh -f' - } - ] - const inspection = { - foregroundProcess: 'zsh', - hasChildProcesses: true, - foregroundProcessEvidence: resolveRemoteForegroundEvidence( - { rootPid: 60404, fallbackProcess: 'zsh' }, - { - ptyId: TRANSCRIPT_PANE_PTY_ID, - ptyIncarnationId: 'inc-1', - authorityGeneration: 'gen-1', - observationEpoch: 1, - capturedAgeMs: 0, - platform: 'darwin' - }, - rows - ) - } - // Presence precondition: the fence is live, and its foreground group is not the root's. - expect(inspection.foregroundProcessEvidence).toMatchObject({ - verdict: 'live', - fence: { shellPid: 60404, foregroundPgid: 60406 } - }) - const { runtime } = await createTranscriptPane({ - paneTitle: 'Terminal', - foregroundProcess: 'python3', - processInspection: inspection, - confirmedForegroundProcess: '-zsh', - launchAgent: 'claude', - data: '' - }) + // A tcsh or nu launch line runs the agent from `/bin/sh '