fix(ssh): preserve process evidence polling semantics

This commit is contained in:
Merge Sim
2026-08-31 11:27:58 -07:00
parent d9f7c9cee0
commit bf758b955e
6 changed files with 42 additions and 15 deletions
@@ -1,4 +1,9 @@
import { recognizeAgentProcessFromCommandLine } from '../../shared/agent-process-recognition'
import {
isAgentForegroundWrapperProcess,
isExpectedAgentProcess,
recognizeAgentProcessFromCommandLine
} from '../../shared/agent-process-recognition'
import { getFirstCommandToken } from '../../shared/command-token-scanner'
import { resolveOuterWrapperForegroundProcess } from '../../shared/foreground-wrapper-agent'
import type { ForegroundProcessEvidence } from '../../shared/foreground-process-evidence'
import {
@@ -103,7 +108,19 @@ export function resolveAgentForegroundProcessesFromIndex(
reason: 'no_controlling_tty'
}
}
const candidates = (rowsByOwner.get(root.pid) ?? []).filter((row) => row.pgid === root.tpgid)
const allCandidates = rowsByOwner.get(root.pid) ?? []
const foregroundCandidates = allCandidates.filter((row) => row.pgid === root.tpgid)
const fallbackProcess = request.fallbackProcess
const wrapperFallback =
typeof fallbackProcess === 'string' && isAgentForegroundWrapperProcess(fallbackProcess)
const candidates = wrapperFallback
? foregroundCandidates.filter((candidate) =>
isExpectedAgentProcess(getFirstCommandToken(candidate.command), fallbackProcess)
)
: foregroundCandidates
if (wrapperFallback && candidates.length !== 1) {
return { available: true, processName: null }
}
let bestCandidate: (ProcessTableRow & { depth: number }) | null = null
let bestName: ReturnType<typeof recognizeAgentProcessFromCommandLine> = null
for (const candidate of candidates) {
@@ -119,7 +136,7 @@ export function resolveAgentForegroundProcessesFromIndex(
if (bestCandidate && bestName) {
return {
available: true,
processName: resolveOuterWrapperForegroundProcess(bestName, bestCandidate, candidates)
processName: resolveOuterWrapperForegroundProcess(bestName, bestCandidate, allCandidates)
}
}
return { available: true, processName: null }
+2 -6
View File
@@ -13,7 +13,6 @@ import {
resolveProcessCwd,
processHasChildren,
getForegroundProcessName,
getForegroundProcessNameFromProcessTable,
isProcessAlive,
listShellProfiles
} from './pty-shell-utils'
@@ -2286,13 +2285,10 @@ export class PtyHandler {
this.reapExitedPty(managed)
continue
}
// Reuse batched correlation; per-PTY tree scans recreate O(PTY × rows) work.
const title =
(evidenceRows
? getForegroundProcessNameFromProcessTable(
[...evidenceRows],
managed.pty.pid,
managed.pty.process || null
)
? (evidenceResults[entryIndex]?.processName ?? managed.pty.process ?? null)
: await getForegroundProcessName(managed.pty.pid, managed.pty.process || null)) || 'shell'
const foregroundProcessEvidence =
process.platform !== 'win32'
@@ -124,6 +124,20 @@ describe('agent completion no-evidence inspection cadence', () => {
expect(inspectProcess).toHaveBeenCalledTimes(4)
})
it('does not re-arm no-evidence scans for output from hidden panes', async () => {
const inspectProcess = vi.fn(async () => processResult(null, false))
const { coordinator } = createCoordinator(inspectProcess, {
shouldPollProcessCadence: () => false,
shouldPollNoEvidenceProcessCadence: () => false
})
coordinator.startProcessTracking()
coordinator.observeOutputActivity()
await vi.advanceTimersByTimeAsync(60_000)
expect(inspectProcess).not.toHaveBeenCalled()
})
it('escalates to the hot cadence when PTY output appears mid-interval', async () => {
const inspectProcess = vi.fn(async () => processResult(null, false))
const { coordinator } = createCoordinator(inspectProcess)
@@ -202,7 +202,8 @@ export function createAgentCompletionProcessMonitor({
state.lastForegroundAgent !== null ||
(options.shouldPollProcessCadence?.() !== false &&
options.shouldPollNoEvidenceProcessCadence?.() !== false) ||
(state.lastPaneActivityAt !== null &&
(options.shouldPollProcessCadence?.() !== false &&
state.lastPaneActivityAt !== null &&
Date.now() - state.lastPaneActivityAt < NO_EVIDENCE_ACTIVITY_HOT_WINDOW_MS)
)
}
@@ -229,12 +229,11 @@ export function installTerminalKeydownFit(session: ConnectPanePtySession): void
}),
shouldPollProcessCadence: () =>
isAgentTaskCompleteTrackingEnabled() && session.deps.isVisibleRef.current,
shouldPollNoEvidenceProcessCadence: () =>
!isRemoteExecutionHostPtyId(session.transport.getPtyId()),
isProcessInspectionCostly: () => {
// Why: local Windows inspection forks a powershell.exe whole-process-table
// CIM scan per poll (~10-40x heavier than POSIX `ps`). Remote authorities
// use the zero-idle cadence gate above instead.
// CIM scan per poll (~10-40x heavier than POSIX `ps`). Keep the no-evidence
// cadence enabled until inventory evidence is consumed by this renderer;
// mixed-version relays may omit the optional field.
if (!navigator.userAgent.includes('Windows')) {
return false
}
+1 -1
View File
@@ -322,7 +322,7 @@ const strictReader = createProcessTableSnapshotReader<ProcessTableRow[]>({
})
/**
* Run (or reuse a recent) `ps -axo pid=,ppid=,stat=,command=` scan and return
* Run (or reuse a recent) `ps -axo` process-table scan and return
* its parsed rows. Per-process singleton: the relay and local main processes
* each dedupe their own scans and share a single parse per TTL window.
*/