mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
fix(agent-launch): judge the foreground-group proof by when its capture began, not how long ps took
The age the host stamps on a process-group observation runs from the start of its whole-machine ps, so on a loaded Mac a capture begun after the read was asked for still read as older than 1 s and the proof was dropped. Count an observation whose capture began after the read was asked for, less the window a shared capture is reused across.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { RemoteForegroundEvidence } from '../../shared/foreground-process-evidence'
|
||||
import { readLaunchedAgentForeground } from './launched-agent-foreground'
|
||||
|
||||
/** A live observation naming `claude` in the terminal's foreground group. */
|
||||
function claudeInGroup(capturedAgeMs: number): RemoteForegroundEvidence {
|
||||
return {
|
||||
verdict: 'live',
|
||||
processName: 'claude',
|
||||
fence: {
|
||||
platform: 'posix',
|
||||
shellPid: 40100,
|
||||
shellStartTime: 'Fri Oct 2 09:30:01 2026',
|
||||
tty: 'ttys007',
|
||||
foregroundPgid: 40210
|
||||
},
|
||||
authorityGeneration: 'gen-1',
|
||||
observationEpoch: 1,
|
||||
capturedAgeMs,
|
||||
ptyId: 'pty-1',
|
||||
ptyIncarnationId: 'inc-1'
|
||||
}
|
||||
}
|
||||
|
||||
/** The fresh scan names the `sh` that leads the group, as it does behind a wrapper. */
|
||||
function controllerAnswering(inspect: () => Promise<RemoteForegroundEvidence>) {
|
||||
return {
|
||||
getForegroundProcess: async () => 'sh',
|
||||
confirmForegroundProcess: async () => 'sh',
|
||||
confirmShellForeground: async () => false,
|
||||
inspectProcess: async () => ({
|
||||
foregroundProcess: 'sh',
|
||||
hasChildProcesses: true,
|
||||
foregroundProcessEvidence: await inspect()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const POSIX_LOCAL = { remote: false, windows: false }
|
||||
|
||||
describe('the foreground group as proof of a launched agent', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
// On a loaded host the whole-machine capture can take seconds; it still describes the moment
|
||||
// after the read was asked for.
|
||||
it('counts a capture begun after the read was asked for, however long `ps` took', async () => {
|
||||
const controller = controllerAnswering(async () => {
|
||||
vi.advanceTimersByTime(1_800)
|
||||
return claudeInGroup(1_800)
|
||||
})
|
||||
|
||||
await expect(
|
||||
readLaunchedAgentForeground(controller, POSIX_LOCAL, 'pty-1', 'claude')
|
||||
).resolves.toBe('agent')
|
||||
})
|
||||
|
||||
it('takes the fresh read over a capture reused from before the read was asked for', async () => {
|
||||
const controller = controllerAnswering(async () => claudeInGroup(1_500))
|
||||
|
||||
await expect(
|
||||
readLaunchedAgentForeground(controller, POSIX_LOCAL, 'pty-1', 'claude')
|
||||
).resolves.toBe('shell')
|
||||
})
|
||||
})
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
isExpectedAgentProcess,
|
||||
recognizeAgentProcess
|
||||
} from '../../shared/agent-process-recognition'
|
||||
import { PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS } from '../../shared/process-table-snapshot'
|
||||
import { isShellProcess } from '../../shared/shell-process-detection'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import { TUI_AGENT_CONFIG } from '../../shared/tui-agent-config'
|
||||
@@ -14,9 +15,6 @@ 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(/^-/, ''))
|
||||
@@ -33,8 +31,10 @@ function isLaunchedAgent(processName: string, agent: TuiAgent): boolean {
|
||||
* 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.
|
||||
* group. It counts only from a capture begun after this read was asked for, less the window a
|
||||
* shared capture is reused across, and never by how long `ps` took on a loaded host. 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
|
||||
@@ -65,11 +65,12 @@ 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 askedAt = Date.now()
|
||||
const evidence = (await controller.inspectProcess?.(ptyId))?.foregroundProcessEvidence
|
||||
if (
|
||||
evidence?.verdict === 'live' &&
|
||||
evidence.fence.platform === 'posix' &&
|
||||
evidence.capturedAgeMs <= FOREGROUND_GROUP_EVIDENCE_MAX_AGE_MS &&
|
||||
Date.now() - evidence.capturedAgeMs >= askedAt - PROCESS_TABLE_SNAPSHOT_MAX_STALENESS_MS &&
|
||||
evidence.processName &&
|
||||
isLaunchedAgent(evidence.processName, agent)
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user