mirror of
https://github.com/stablyai/orca.git
synced 2026-09-25 16:02:38 +00:00
* perf(terminal): cheap-tier process inspection for anchored local agent panes Every idle local pane's completion cadence ran a full whole-host `ps` (with `tty=` and `command=`, 0.34-0.50s on a 1,900-process Mac, 1.15s on Linux) purely to build `foregroundProcessEvidence` that the renderer then discards for local ids. Add a cheap tier (same job-control columns, no tty/command, 0.03s) gated so that it introduces no user-facing trade-off: - Only a pane whose last FULL capture proved a recognized agent may take the cheap tier. Panes with no anchor always take the full capture, so start discovery keeps today's exact behaviour. - The cheap tick compares a per-pane fingerprint (root shell pid+start, tpgid, every descendant's pid+start+pgid+job-control state). Any change, a changed node-pty foreground name, an unreadable capture, or an incarnation mismatch escalates to the full capture. A recognized agent's exit is always a pid vanishing, which the fingerprint always sees. - A cheap answer OMITS evidence rather than fabricating a tty-less fence. Remote/restore consumers never send `steadyState`, so they keep the full capture unchanged. - `steadyState` is a new optional request field; an old daemon ignores it and answers with the full capture. Measured (8 idle panes, 60s, idle cadence, forks counted by column set): 30 full -> 1 full + 29 cheap. * fix(terminal): route the cheap ps capture through runProcess The cheap-tier reader imported node:child_process directly, which the child-process import-boundary and windowsHide ratchet tests reject (CI shards 1/8 and 3/8). Use Orca's single spawn entry point instead; it pins windowsHide and encodes argv. Map its result onto the capture-error vocabulary: outputTruncated -> capture_truncated, timedOut -> capture_timeout, non-zero exit -> ps_exit_<code>. Tests mock at the runProcess seam. * fix(perf): refuse a pane fingerprint when any descendant start marker is missing `buildPaneProcessFingerprint` rejected only a missing root start marker; a missing descendant marker was stamped as `?`. Two captures that both failed to read the same descendant therefore compared equal, which removes the pid-reuse protection the fingerprint exists to provide: a recycled pid could make a vanished agent look unchanged, and the cheap tier would keep serving its name instead of escalating. Reachable on Linux, where `readLinuxProcStartTime` legitimately returns null when a process exits between the `ps` capture and the `/proc/<pid>/stat` read. Every subtree member now needs a start marker or the fingerprint is refused, which sends the caller to the full capture — the same conservative default every other uncertain path takes. Reported by CodeRabbit on #18780. The two new tests fail against the previous code with `expected '4242@2400#4300:|4300@?:4300:+' to be null`.
52 lines
1.8 KiB
TypeScript
52 lines
1.8 KiB
TypeScript
import { runProcess } from './child-process/run-process'
|
|
import {
|
|
CHEAP_PS_ARGS,
|
|
PS_MAX_BUFFER_BYTES,
|
|
ProcessTableCaptureError,
|
|
parseCheapProcessTableRows,
|
|
type CheapProcessTableRow
|
|
} from './process-table-snapshot'
|
|
import {
|
|
PS_TIMEOUT_MS,
|
|
createProcessTableSnapshotReader,
|
|
withEvidenceBudget
|
|
} from './process-table-snapshot-reader'
|
|
|
|
/**
|
|
* The cheap-tier sibling of the strict evidence reader: same coalescing and TTL, a
|
|
* column set without `tty=`/`command=`. Separate instance because the two column sets
|
|
* parse differently and a cheap capture must never be served to an evidence consumer.
|
|
*/
|
|
const cheapProcessTableReader = createProcessTableSnapshotReader<CheapProcessTableRow[]>({
|
|
runPs: async () => {
|
|
const result = await runProcess({
|
|
program: 'ps',
|
|
args: CHEAP_PS_ARGS,
|
|
timeoutMs: PS_TIMEOUT_MS,
|
|
maxOutputBytes: PS_MAX_BUFFER_BYTES
|
|
})
|
|
// A ceiling hit is truncation, not absence: name it in the domain vocabulary.
|
|
if (result.outputTruncated) {
|
|
throw new ProcessTableCaptureError('capture_truncated')
|
|
}
|
|
if (result.timedOut) {
|
|
throw new ProcessTableCaptureError('capture_timeout')
|
|
}
|
|
if (result.code !== 0) {
|
|
throw new ProcessTableCaptureError(`ps_exit_${result.code ?? result.signal ?? 'unknown'}`)
|
|
}
|
|
return parseCheapProcessTableRows(result.stdout)
|
|
},
|
|
now: () => Date.now()
|
|
})
|
|
|
|
/** Same wait bound as the evidence read: a stalled cheap capture must fall through to the full
|
|
* path's own handling rather than pin a polled tick. */
|
|
export async function getCheapProcessTableSnapshot(): Promise<CheapProcessTableRow[]> {
|
|
return withEvidenceBudget(cheapProcessTableReader.getSnapshot())
|
|
}
|
|
|
|
export function resetCheapProcessTableSnapshotForTests(): void {
|
|
cheapProcessTableReader.reset()
|
|
}
|