Files
orca/src/shared/foreground-wrapper-agent.test.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

56 lines
2.0 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { resolveOuterWrapperForegroundProcess } from './foreground-wrapper-agent'
describe('resolveOuterWrapperForegroundProcess', () => {
const omp = { agent: 'omp' as const, processName: 'omp' }
const pi = { agent: 'pi' as const, processName: 'pi' }
it('collapses a wrapped pi read onto the shallower omp wrapper', () => {
// Winner is the deeper pi (depth 2); omp is its wrapper at depth 1.
expect(
resolveOuterWrapperForegroundProcess(pi, { pid: 102, ppid: 101, command: 'pi' }, [
{ pid: 102, ppid: 101, command: 'pi' },
{ pid: 101, ppid: 100, command: 'omp' }
])
).toBe('omp')
})
it('keeps bare pi when no same-group wrapper is present', () => {
const barePi = { pid: 101, ppid: 100, command: 'pi' }
expect(resolveOuterWrapperForegroundProcess(pi, barePi, [barePi])).toBe('pi')
})
it('leaves a cross-group agent (codex) untouched even under a deeper same-name child', () => {
const codex = { agent: 'codex' as const, processName: 'codex' }
expect(
resolveOuterWrapperForegroundProcess(
codex,
{ pid: 102, ppid: 101, command: 'node /usr/bin/codex' },
[
{ pid: 102, ppid: 101, command: 'node /usr/bin/codex' },
{ pid: 101, ppid: 100, command: 'bash -l' }
]
)
).toBe('codex')
})
it('does not promote a deeper pi over an already-outer omp winner', () => {
expect(
resolveOuterWrapperForegroundProcess(omp, { pid: 101, ppid: 100, command: 'omp' }, [
{ pid: 101, ppid: 100, command: 'omp' },
{ pid: 102, ppid: 101, command: 'pi' }
])
).toBe('omp')
})
it('does not treat an unrelated shallower omp sibling as the pi wrapper', () => {
expect(
resolveOuterWrapperForegroundProcess(pi, { pid: 103, ppid: 102, command: 'pi' }, [
{ pid: 101, ppid: 100, command: 'omp' },
{ pid: 102, ppid: 100, command: 'node server.js' },
{ pid: 103, ppid: 102, command: 'pi' }
])
).toBe('pi')
})
})