mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
Merge #23962 head ef629543bd (#24257 3054018722: agent proven in front behind a wrapper) into the argv-carry step
This commit is contained in:
@@ -265,6 +265,77 @@ describe('what holds a launched agent’s terminal', () => {
|
||||
runtime.readLaunchedAgentForeground(TRANSCRIPT_PANE_PTY_ID, 'copilot')
|
||||
).resolves.toBe('shell')
|
||||
})
|
||||
|
||||
// A tcsh or nu launch line runs the agent from `/bin/sh '<script>'`, which leads the terminal's
|
||||
// foreground group with the agent a member of it, so the scan names `sh`.
|
||||
const shLeadsClaude = (capturedAgeMs: number, agentCommand: string) => {
|
||||
const row = (pid: number, ppid: number, stat: string, command: string): ProcessTableRow => ({
|
||||
pid,
|
||||
ppid,
|
||||
pgid: 40210,
|
||||
tpgid: 40210,
|
||||
stat,
|
||||
tty: 'ttys007',
|
||||
startTime: 'Fri Oct 2 09:30:01 2026',
|
||||
command
|
||||
})
|
||||
return {
|
||||
foregroundProcess: 'sh',
|
||||
hasChildProcesses: true,
|
||||
foregroundProcessEvidence: resolveRemoteForegroundEvidence(
|
||||
{ rootPid: 40100, fallbackProcess: 'sh' },
|
||||
{
|
||||
ptyId: TRANSCRIPT_PANE_PTY_ID,
|
||||
ptyIncarnationId: 'inc-1',
|
||||
authorityGeneration: 'gen-1',
|
||||
observationEpoch: 1,
|
||||
capturedAgeMs,
|
||||
platform: 'darwin'
|
||||
},
|
||||
[
|
||||
{ ...row(40100, 40090, 'Ss', '-tcsh'), pgid: 40100 },
|
||||
row(40210, 40100, 'S+', '/bin/sh /tmp/orca-launch/run.sh'),
|
||||
row(40211, 40210, 'S+', agentCommand)
|
||||
]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
it.each([
|
||||
[
|
||||
'finds the launched agent behind the sh that leads its group',
|
||||
0,
|
||||
'/opt/bin/claude',
|
||||
'agent'
|
||||
],
|
||||
// Older than the quiet a ready signal needs, it may predate the agent's exit.
|
||||
[
|
||||
'takes the fresh read over an observation too old to trust',
|
||||
1_500,
|
||||
'/opt/bin/claude',
|
||||
'shell'
|
||||
],
|
||||
['takes the fresh read when the group holds another agent', 0, '/opt/bin/codex', 'shell']
|
||||
] as const)('%s', async (_label, capturedAgeMs, agentCommand, found) => {
|
||||
const inspection = shLeadsClaude(capturedAgeMs, agentCommand)
|
||||
// Presence precondition: the observation is live and names the agent in the group.
|
||||
expect(inspection.foregroundProcessEvidence).toMatchObject({
|
||||
verdict: 'live',
|
||||
processName: expect.any(String)
|
||||
})
|
||||
const { runtime } = await createTranscriptPane({
|
||||
paneTitle: 'Terminal',
|
||||
foregroundProcess: 'sh',
|
||||
processInspection: inspection,
|
||||
confirmedForegroundProcess: 'sh',
|
||||
launchAgent: 'claude',
|
||||
data: ''
|
||||
})
|
||||
|
||||
await expect(
|
||||
runtime.readLaunchedAgentForeground(TRANSCRIPT_PANE_PTY_ID, 'claude')
|
||||
).resolves.toBe(found)
|
||||
})
|
||||
})
|
||||
|
||||
// The scan names the pane's shell for an agent it cannot recognize (an npm agent as `node.exe`),
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { isExpectedAgentProcess } from '../../shared/agent-process-recognition'
|
||||
import {
|
||||
isExpectedAgentProcess,
|
||||
recognizeAgentProcess
|
||||
} from '../../shared/agent-process-recognition'
|
||||
import { isShellProcess } from '../../shared/shell-process-detection'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import { TUI_AGENT_CONFIG } from '../../shared/tui-agent-config'
|
||||
@@ -11,17 +14,32 @@ import type { RuntimePtyController } from './runtime-pty-controller-contract'
|
||||
*/
|
||||
export type LaunchedAgentForeground = 'agent' | 'shell' | 'unknown'
|
||||
|
||||
/** Under the 1.5 s quiet a ready signal needs, so an observation this young postdates a crash. */
|
||||
const FOREGROUND_GROUP_EVIDENCE_MAX_AGE_MS = 1_000
|
||||
|
||||
/** A login shell is reported as `-zsh`. */
|
||||
function isLaunchShell(processName: string): boolean {
|
||||
return isShellProcess(processName.replace(/^-/, ''))
|
||||
}
|
||||
|
||||
function isLaunchedAgent(processName: string, agent: TuiAgent): boolean {
|
||||
return (
|
||||
recognizeAgentProcess(processName)?.agent === agent ||
|
||||
isExpectedAgentProcess(processName, TUI_AGENT_CONFIG[agent].expectedProcess)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* One fresh read of the terminal's foreground process: on a local POSIX host the scan behind
|
||||
* `confirmForegroundProcess`, and on an SSH host the relay's name, which it reads from the terminal
|
||||
* when asked. Never the cached name a tab icon uses: it can still name a process that already
|
||||
* exited. Not the process-group fence either: a macOS pane runs its shell under `login`, so the
|
||||
* fence's root is never the shell's group.
|
||||
* First the host's process-group observation, which only ever proves the agent: the launched agent
|
||||
* among the members of the terminal's foreground group finds it behind a wrapper that did not
|
||||
* `exec` it (a script, or the `/bin/sh` a tcsh or nu launch line runs), whose own name leads the
|
||||
* group. It never proves a shell: a macOS pane's shell runs under `login`, so the group's root is
|
||||
* not the shell, and a capture that ran out of time is no answer.
|
||||
*
|
||||
* Otherwise one fresh read of the terminal's foreground process: on a local POSIX host the scan
|
||||
* behind `confirmForegroundProcess`, and on an SSH host the relay's name, which it reads from the
|
||||
* terminal when asked. Never the cached name a tab icon uses: it can still name a process that
|
||||
* already exited.
|
||||
*
|
||||
* Windows has no foreground process group, and its scan names the pane's shell for an agent it
|
||||
* cannot recognize, while Git Bash and WSL keep other processes in the shell's job, so nothing
|
||||
@@ -30,7 +48,10 @@ function isLaunchShell(processName: string): boolean {
|
||||
export async function readLaunchedAgentForeground(
|
||||
controller: Pick<
|
||||
RuntimePtyController,
|
||||
'getForegroundProcess' | 'confirmForegroundProcess' | 'confirmShellForeground'
|
||||
| 'getForegroundProcess'
|
||||
| 'confirmForegroundProcess'
|
||||
| 'confirmShellForeground'
|
||||
| 'inspectProcess'
|
||||
> | null,
|
||||
host: { remote: boolean; windows: boolean },
|
||||
ptyId: string,
|
||||
@@ -44,6 +65,16 @@ export async function readLaunchedAgentForeground(
|
||||
// An SSH pane has no such check, so on a Windows relay nothing proves a shell.
|
||||
return (await controller.confirmShellForeground?.(ptyId)) ? 'shell' : 'unknown'
|
||||
}
|
||||
const evidence = (await controller.inspectProcess?.(ptyId))?.foregroundProcessEvidence
|
||||
if (
|
||||
evidence?.verdict === 'live' &&
|
||||
evidence.fence.platform === 'posix' &&
|
||||
evidence.capturedAgeMs <= FOREGROUND_GROUP_EVIDENCE_MAX_AGE_MS &&
|
||||
evidence.processName &&
|
||||
isLaunchedAgent(evidence.processName, agent)
|
||||
) {
|
||||
return 'agent'
|
||||
}
|
||||
const foreground = host.remote
|
||||
? await controller.getForegroundProcess(ptyId)
|
||||
: ((await controller.confirmForegroundProcess?.(ptyId)) ?? null)
|
||||
|
||||
Reference in New Issue
Block a user