Files
orca/src/shared/foreground-wrapper-agent.ts
T
Brennan Benson f655ae3929 fix(pi): resolve OMP tab identity from the outer wrapper, not the wrapped pi (#6364) (#9600)
* fix(pi): resolve OMP tab identity from the outer wrapper, not the wrapped pi (#6364)

OMP runs as a `shell → omp → pi` process tree and Orca recognizes both `omp`
and `pi` as distinct agents. The foreground-process readers scored the deepest
`+` foreground descendant, so they returned the wrapped `pi` engine instead of
the `omp` the user actually launched — and because the sampled `+` frame moves
between omp and pi across command boundaries, the read oscillated and the tab
icon/label flickered OMP↔Pi. It surfaced on Remote Host / SSH (only in a
worktree, only while working) where a mirrored/restored pane has no client-side
launchAgent to anchor identity, so the (host-read) foreground was the only
pi/omp signal — and it was wrong.

Fix the detection at its source so foregroundAgent is trustworthy everywhere
(local tab icon, exit detection, and the remote mirror):

- Add `resolveOuterWrapperForegroundProcess` (src/shared/foreground-wrapper-agent.ts):
  when the recognized foreground winner shares a `titleIdentityGroup` with a
  shallower recognized ancestor, return the shallowest — the outer wrapper.
  Stable regardless of which of omp/pi holds the foreground at the sample; bare
  Pi and cross-group reads (Codex, etc.) are unchanged.
- Wire it into both readers: local `agent-foreground-process.ts` and the SSH
  relay `pty-shell-utils.ts`. Also harden the relay's fallback short-circuit so a
  pi-compatible process reported by node-pty rescans for the omp wrapper instead
  of trusting `pi` raw (never downgrading below the fallback).

Also unify the host-side publish path onto the shared owner resolver: the mobile
session snapshot builders now resolve pane ownership via `resolvePaneAgentOwner`
({launchAgent, hookAgent}) instead of a bespoke `launchAgent ?? foregroundAgent`,
so the host stops being a divergent fourth identity path and the hook's
host-stamped identity survives when launchAgent is dropped on a mirrored pane.

Tests: unit coverage for the wrapper resolver, `shell→omp→pi` ps trees for both
readers (outer omp, oscillating-frame, bare pi, node-pty pi fallback), and a
host-snapshot regression that keeps an OMP hook labeled OMP when the wrapped pi
child owns the foreground.

* fix(pi): harden wrapper foreground ownership

* fix(pi): preserve OMP ownership in daemon foreground reads

* fix(pi): retain OMP owner when process scans fail
2026-07-20 16:16:34 -07:00

57 lines
1.8 KiB
TypeScript

import {
recognizeAgentProcessFromCommandLine,
type RecognizedAgentProcess
} from './agent-process-recognition'
import { getSyntheticAgentTitleProfile } from './synthetic-agent-title'
export type ForegroundAgentCandidate = {
pid: number
ppid: number
command: string
name?: string
}
export function shouldInspectOuterWrapperForegroundProcess(
process: RecognizedAgentProcess
): boolean {
// Why: only Pi is currently embedded by a same-group wrapper; scanning OMP would add a subprocess to every relay poll.
return process.agent === 'pi'
}
/**
* Collapse a foreground read onto its outermost same-title-group ancestor.
* Why: OMP embeds Pi, while depth alone cannot distinguish wrappers from sibling jobs.
*/
export function resolveOuterWrapperForegroundProcess(
winner: RecognizedAgentProcess,
winnerCandidate: ForegroundAgentCandidate,
descendants: readonly ForegroundAgentCandidate[]
): string {
const winnerGroup = getSyntheticAgentTitleProfile(winner.agent)?.titleIdentityGroup
if (!winnerGroup) {
return winner.processName
}
const candidatesByPid = new Map(descendants.map((candidate) => [candidate.pid, candidate]))
const seen = new Set<number>([winnerCandidate.pid])
let outerProcessName = winner.processName
let parentPid = winnerCandidate.ppid
while (!seen.has(parentPid)) {
seen.add(parentPid)
const candidate = candidatesByPid.get(parentPid)
if (!candidate) {
break
}
const recognized =
recognizeAgentProcessFromCommandLine(candidate.command) ??
recognizeAgentProcessFromCommandLine(candidate.name)
if (
recognized &&
getSyntheticAgentTitleProfile(recognized.agent)?.titleIdentityGroup === winnerGroup
) {
outerProcessName = recognized.processName
}
parentPid = candidate.ppid
}
return outerProcessName
}