fix(interactive-subagents): avoid Orca polling fan-out
This commit is contained in:
@@ -14,6 +14,7 @@ const execFileAsync = promisify(execFile);
|
||||
|
||||
/** GNOME screen reader path — never a valid Orca CLI. */
|
||||
const GNOME_SCREEN_READER = "/usr/bin/orca";
|
||||
const ASYNC_READ_SCREEN_TIMEOUT_MS = 5_000;
|
||||
|
||||
let cachedCli: string | null | undefined;
|
||||
|
||||
@@ -275,14 +276,26 @@ export function readScreen(surface: string, lines = 50): string {
|
||||
return parseReadTail(output);
|
||||
}
|
||||
|
||||
export async function readScreenAsync(surface: string, lines = 50): Promise<string> {
|
||||
const cli = requireOrca();
|
||||
async function readScreenWithCli(
|
||||
cli: string,
|
||||
surface: string,
|
||||
lines: number,
|
||||
timeout = ASYNC_READ_SCREEN_TIMEOUT_MS,
|
||||
): Promise<string> {
|
||||
const { stdout } = await execFileAsync(cli, readScreenArgs(surface, lines), {
|
||||
encoding: "utf8",
|
||||
timeout,
|
||||
killSignal: "SIGKILL",
|
||||
});
|
||||
return parseReadTail(stdout);
|
||||
}
|
||||
|
||||
export async function readScreenAsync(surface: string, lines = 50): Promise<string> {
|
||||
return readScreenWithCli(requireOrca(), surface, lines);
|
||||
}
|
||||
|
||||
export const __orcaTest__ = { readScreenWithCli };
|
||||
|
||||
export function closeSurface(surface: string): void {
|
||||
const cli = requireOrca();
|
||||
const args = ["terminal", "close", "--terminal", surface];
|
||||
|
||||
@@ -153,18 +153,28 @@ function readCompletionFile(path: string): PollResult | null {
|
||||
return { reason: "sentinel", exitCode };
|
||||
}
|
||||
|
||||
type PollOptions = {
|
||||
interval: number;
|
||||
sessionFile?: string;
|
||||
sentinelFile?: string;
|
||||
completionFile?: string;
|
||||
onTick?: (elapsed: number) => void;
|
||||
};
|
||||
|
||||
/** Avoid launching a terminal CLI reader when a local completion signal is authoritative. */
|
||||
function shouldPollSurface(
|
||||
options: Pick<PollOptions, "sessionFile" | "sentinelFile" | "completionFile">,
|
||||
): boolean {
|
||||
return !options.sessionFile && !options.sentinelFile && !options.completionFile;
|
||||
}
|
||||
|
||||
export const __pollForExitTest__ = { interpretExitSidecar, readCompletionFile };
|
||||
|
||||
export async function pollForExit(
|
||||
surface: string,
|
||||
signal: AbortSignal,
|
||||
options: {
|
||||
interval: number;
|
||||
sessionFile?: string;
|
||||
sentinelFile?: string;
|
||||
completionFile?: string;
|
||||
onTick?: (elapsed: number) => void;
|
||||
},
|
||||
options: PollOptions,
|
||||
readSurface: (surface: string, lines?: number) => Promise<string> = readScreenAsync,
|
||||
): Promise<PollResult> {
|
||||
const start = Date.now();
|
||||
|
||||
@@ -197,12 +207,14 @@ export async function pollForExit(
|
||||
} catch {}
|
||||
}
|
||||
|
||||
try {
|
||||
const screen = await readScreenAsync(surface, 5);
|
||||
const match = screen.match(/__SUBAGENT_DONE_(\d+)__/);
|
||||
if (match) return { reason: "sentinel", exitCode: Number.parseInt(match[1], 10) };
|
||||
} catch {
|
||||
// Surface reads are best-effort. Filesystem completion remains authoritative.
|
||||
if (shouldPollSurface(options)) {
|
||||
try {
|
||||
const screen = await readSurface(surface, 5);
|
||||
const match = screen.match(/__SUBAGENT_DONE_(\d+)__/);
|
||||
if (match) return { reason: "sentinel", exitCode: Number.parseInt(match[1], 10) };
|
||||
} catch {
|
||||
// Surface reads are a best-effort fallback when no completion file is available.
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = Math.floor((Date.now() - start) / 1000);
|
||||
|
||||
Reference in New Issue
Block a user