Files
orca/src/shared/foreground-process-selection.ts
T

97 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
recognizeAgentProcessFromCommandLine,
type RecognizedAgentProcess
} from './agent-status/process/agent-process-recognition'
export type ForegroundProcessCandidate = {
pid: number
ppid: number
command: string
depth: number
stat?: string
}
export type SelectedForegroundProcess = {
candidate: ForegroundProcessCandidate
recognized: RecognizedAgentProcess
}
/** Prevent vendor helpers from stealing an outer agent’s identity within one process lineage. */
export function selectForegroundProcessCandidate(
candidates: readonly ForegroundProcessCandidate[],
ancestryCandidates: readonly ForegroundProcessCandidate[] = candidates
): SelectedForegroundProcess | null {
const recognized = candidates.flatMap((candidate) => {
const agent = recognizeAgentProcessFromCommandLine(candidate.command)
return agent ? [{ candidate, recognized: agent }] : []
})
if (recognized.length === 0) {
return null
}
const agentNames = new Set(recognized.map(({ recognized: agent }) => agent.agent))
if (agentNames.size > 1) {
const candidatesByPid = new Map(
ancestryCandidates.map((candidate) => [candidate.pid, candidate])
)
const outer = [...recognized].sort(
(left, right) => left.candidate.depth - right.candidate.depth
)[0]
if (!outer) {
return null
}
const descendsFromOuter = makeAncestorReachabilityTest(outer.candidate, candidatesByPid)
if (!recognized.every((entry) => descendsFromOuter(entry.candidate))) {
// Distinct sibling agents do not provide a trustworthy identity.
return null
}
return outer
}
return recognized.reduce((best, current) =>
foregroundCandidateScore(current.candidate) > foregroundCandidateScore(best.candidate)
? current
: best
)
}
function foregroundCandidateScore(candidate: ForegroundProcessCandidate): number {
return (candidate.stat?.includes('+') ? 10_000 : 0) + candidate.depth
}
/** Memoize ancestor reachability for one ancestor and process-table snapshot only; rebuild for every fresh capture. */
function makeAncestorReachabilityTest(
ancestor: ForegroundProcessCandidate,
candidatesByPid: ReadonlyMap<number, ForegroundProcessCandidate>
): (descendant: ForegroundProcessCandidate) => boolean {
const reaches = new Map<number, boolean>()
return (descendant) => {
let currentPid = descendant.pid
// Share the verdict across the walk; visited also terminates reparented/wrapped ppid cycles.
const visited = new Set<number>()
let matches = true
while (currentPid !== ancestor.pid) {
const cached = reaches.get(currentPid)
if (cached !== undefined) {
matches = cached
break
}
if (visited.has(currentPid)) {
matches = false
break
}
visited.add(currentPid)
const current = candidatesByPid.get(currentPid)
if (!current) {
matches = false
break
}
currentPid = current.ppid
}
for (const pid of visited) {
reaches.set(pid, matches)
}
return matches
}
}