fix(computer-use): re-enter single worker host

This commit is contained in:
usr-bin-roygbiv
2026-07-25 07:45:56 +00:00
parent a7e988b604
commit 237692deab
19 changed files with 124 additions and 44 deletions
+1 -1
View File
@@ -33,7 +33,7 @@
### Fixed
- Fixed `todo` calls that omit `op` hard-failing validation ("op must be operation to apply (was missing)"): the tool now validates leniently and infers the op for unambiguous payloads (`list` → `init`, `phase`+`items` → `append`, bare `items` on an empty list → `init`); `op` stays required in the schema, and ambiguous op-less calls surface the schema error as a retryable tool error.
- Fixed Codex subscription and proxy models being sent the unsupported native `{ type: "computer" }` declaration based only on model ID. They now receive the callable function-tool fallback, including after switching from native OpenAI Responses history, while explicit endpoint metadata can still opt into the GA contract. Explicit native Codex replays preserve `computer_call`/`computer_call_output` pairing, and normal CLI startup keeps the native desktop worker graph lazy.
- Fixed Codex subscription and proxy models being sent the unsupported native `{ type: "computer" }` declaration based only on model ID. They now receive the callable function-tool fallback, including after switching from native OpenAI Responses history, while explicit endpoint metadata can still opt into the GA contract. Explicit native Codex replays preserve `computer_call`/`computer_call_output` pairing, and normal CLI startup keeps the native desktop worker graph lazy while packaged workers re-enter the single CLI host entry.
- Fixed isolated JavaScript eval subprocesses letting the global fatal-rejection handler race the cell rejection interceptor. A floated promise rejection is now folded into the owning cell result without killing its reusable worker process.
- Fixed credential-free web search engines (SearXNG, DuckDuckGo, Google, Startpage, Ecosia, Mojeek, and the Public Web fan-out) returning zero results for queries with `site:` paths (e.g. `site:github.com/owner/repo`) or `inurl:` operators: scraper engines only match `site:` against a bare domain and DuckDuckGo ignores `inurl:` entirely, so such queries silently emptied the result set and fell through to the next provider in the chain. A shared `formatScraperQuery` formatter now structurally demotes path-carrying `site:` and all `inurl:` values to plain search terms (covering OR-grouped and quoted directives) while preserving bare-domain `site:` filters, negated operators, and each engine's supported syntax; the pipeline post-filter still enforces the demoted constraints on returned sources.
- Fixed `ast_edit` previews reading like applied edits to the model: the `⟨proposed⟩` badge was TUI-only, so the model-visible result (hashline header + `-`/`+` rows, identical to applied edit output) carried no staged-proposal signal. The preview result now leads with a "Staged as a proposal — files NOT modified yet" notice naming `xd://resolve`/`xd://reject`, the injected resolve reminder names the source tool, and the `ast_edit` tool prompt documents the two-phase flow.
-1
View File
@@ -106,7 +106,6 @@
"files": [
"src",
"dist/cli.js",
"dist/computer-worker-process-entry.js",
"dist/*.node",
"scripts",
"examples",
@@ -96,7 +96,6 @@ async function main(): Promise<void> {
await compileCodingAgent({
repoRoot,
entrypoint: path.join(packageDir, "src", "cli.ts"),
workerEntrypoints: [path.join(packageDir, "src", "computer-worker-process-entry.ts")],
outfile: outputPath,
transformersVersion,
target: crossBuild?.target,
+1 -5
View File
@@ -78,7 +78,6 @@ async function cleanBundleOutputs(): Promise<void> {
.filter(
entry =>
entry === "cli.js" ||
entry === "computer-worker-process-entry.js" ||
entry.endsWith(".node") ||
entry.endsWith(".js.map"),
)
@@ -98,10 +97,7 @@ async function main(): Promise<void> {
// 128KiB per-argv-string cap, so it can never be passed as a CLI
// `--define` (posix_spawn fails with E2BIG).
const output = await Bun.build({
entrypoints: [
path.join(packageDir, "src/cli.ts"),
path.join(packageDir, "src/computer-worker-process-entry.ts"),
],
entrypoints: [path.join(packageDir, "src/cli.ts")],
outdir: outDir,
target: "bun",
external: [...ALWAYS_EXTERNAL, ...RUNTIME_EXTERNAL],
@@ -10,8 +10,6 @@ export interface CodingAgentCompileOptions {
readonly repoRoot: string;
/** Absolute CLI entrypoint. */
readonly entrypoint: string;
/** Additional worker modules embedded as independently evaluated process entries. */
readonly workerEntrypoints?: readonly string[];
/** Absolute standalone executable output path. */
readonly outfile: string;
/** Concrete Transformers.js version baked into the tiny-model worker. */
@@ -35,7 +33,7 @@ export async function compileCodingAgent(options: CodingAgentCompileOptions): Pr
}
try {
const output = await Bun.build({
entrypoints: [options.entrypoint, ...(options.workerEntrypoints ?? [])],
entrypoints: [options.entrypoint],
root: options.repoRoot,
external: [...COMPILED_EXTERNAL_DEPENDENCIES],
define: {
+7
View File
@@ -32,6 +32,7 @@ import { startJsEvalProcess } from "./eval/js/process-entry";
import type { WorkerInbound as JsWorkerInbound, WorkerOutbound as JsWorkerOutbound } from "./eval/js/worker-protocol";
import { DAEMON_BROKER_WORKER_ARG } from "./launch/protocol";
import { smokeTestComputerWorker } from "./tools/computer/supervisor";
import { startComputerWorker } from "./tools/computer/worker-entry";
if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {
process.stderr.write(
@@ -112,6 +113,7 @@ async function runSmokeTest(): Promise<void> {
const TINY_WORKER_ARG = "__omp_worker_tiny_inference";
const STATS_SYNC_WORKER_ARG = "__omp_worker_stats_sync";
const TAB_WORKER_ARG = "__omp_worker_tab";
const COMPUTER_WORKER_ARG = "__omp_worker_computer";
const JS_EVAL_WORKER_ARG = "__omp_worker_js_eval";
const JS_EVAL_PROCESS_ARG = "__omp_worker_js_eval_process";
const STT_WORKER_ARG = "__omp_worker_stt";
@@ -155,6 +157,11 @@ async function runWorkerEntrypoint(arg: string | undefined): Promise<boolean> {
await import("./tools/browser/tab-worker-entry");
return true;
}
if (arg === COMPUTER_WORKER_ARG) {
if (parentPort) installWorkerInbox(parentPort);
startComputerWorker();
return true;
}
if (arg === JS_EVAL_WORKER_ARG) {
if (parentPort) installWorkerInbox(parentPort);
await import("./eval/js/worker-entry");
@@ -1,8 +0,0 @@
import { parentPort } from "node:worker_threads";
import { installWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
import { startComputerWorker } from "./tools/computer/worker-entry";
if (!parentPort) throw new Error("computer-worker-process-entry: missing parentPort");
installWorkerInbox(parentPort);
startComputerWorker();
@@ -1,5 +1,6 @@
import { expect, it } from "bun:test";
import * as path from "node:path";
import * as fs from "node:fs";
import { TempDir } from "@oh-my-pi/pi-utils";
it("imports the CLI entry graph without loading dotenv before profile bootstrap", async () => {
@@ -26,7 +27,27 @@ it("imports the CLI entry graph without loading dotenv before profile bootstrap"
expect(stderr).toBe("");
});
it("starts ordinary CLI paths without evaluating the computer worker entry", async () => {
async function pingComputerWorker(
entry: string,
id: string,
argv: string[] = ["__omp_worker_computer"],
): Promise<unknown> {
const worker = new Worker(entry, {
type: "module",
argv,
});
const response = Promise.withResolvers<unknown>();
worker.addEventListener("message", event => response.resolve(event.data));
worker.addEventListener("error", event => response.reject(event.error ?? new Error(event.message)));
worker.postMessage({ type: "ping", id });
try {
return await response.promise;
} finally {
worker.terminate();
}
}
it("starts ordinary CLI paths without loading the native computer addon", async () => {
const cliPath = path.resolve(import.meta.dir, "../../cli.ts");
for (const args of [
["--no-addons", cliPath, "--version"],
@@ -41,9 +62,9 @@ it("starts ordinary CLI paths without evaluating the computer worker entry", asy
}
});
it("dispatches the computer worker through its dedicated process entry", async () => {
const fixture = path.resolve(import.meta.dir, "../../../test/fixtures/computer-worker-process-entry.ts");
const proc = Bun.spawn([process.execPath, fixture], {
it("dispatches the computer worker through the CLI host selector in a child process", async () => {
const fixture = path.resolve(import.meta.dir, "../../../test/fixtures/computer-worker-cli-selector.ts");
const proc = Bun.spawn([process.execPath, "--no-addons", fixture], {
stdout: "pipe",
stderr: "pipe",
});
@@ -53,5 +74,33 @@ it("dispatches the computer worker through its dedicated process entry", async (
new Response(proc.stderr).text(),
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toBe('{"type":"pong","id":"computer-process-entry"}\n');
expect(stdout).toBe('{"type":"pong","id":"computer-cli-selector"}\n');
});
it("loads the computer worker module directly outside a declared CLI host", async () => {
const entry = new URL("../../tools/computer/worker-entry.ts", import.meta.url).href;
const response = await pingComputerWorker(entry, "computer-direct-module", []);
expect(response).toEqual({ type: "pong", id: "computer-direct-module" });
});
it("dispatches the computer worker from a single npm-style host bundle", async () => {
const packageDir = path.resolve(import.meta.dir, "../../..");
const outDir = fs.mkdtempSync(path.join(packageDir, ".computer-worker-bundle-"));
try {
const output = await Bun.build({
entrypoints: [path.join(packageDir, "test/fixtures/computer-worker-bundled-host.ts")],
outdir: outDir,
naming: "cli.js",
target: "bun",
external: ["@oh-my-pi/pi-natives"],
define: { "process.env.PI_BUNDLED": JSON.stringify("true") },
throw: false,
});
expect(output.logs).toEqual([]);
expect(output.outputs.map(file => path.basename(file.path))).toEqual(["cli.js"]);
const response = await pingComputerWorker(output.outputs[0]!.path, "computer-npm-bundle");
expect(response).toEqual({ type: "pong", id: "computer-npm-bundle" });
} finally {
fs.rmSync(outDir, { recursive: true, force: true });
}
});
@@ -1,5 +1,7 @@
import type { DesktopAction, DesktopCapabilities, DesktopCapture, DesktopSessionOptions } from "@oh-my-pi/pi-natives";
export const COMPUTER_WORKER_ARG = "__omp_worker_computer";
export type ComputerWorkerInbound =
| { type: "ping"; id: string }
| { type: "init"; options: DesktopSessionOptions }
@@ -1,8 +1,14 @@
import type { DesktopAction, DesktopCapabilities, DesktopCapture, DesktopSessionOptions } from "@oh-my-pi/pi-natives";
import { withTimeout } from "@oh-my-pi/pi-utils/async";
import * as logger from "@oh-my-pi/pi-utils/logger";
import { workerHostEntry } from "@oh-my-pi/pi-utils/worker-host";
import { ToolAbortError, ToolError } from "../tool-errors";
import type { ComputerWorkerError, ComputerWorkerInbound, ComputerWorkerOutbound } from "./protocol";
import {
COMPUTER_WORKER_ARG,
type ComputerWorkerError,
type ComputerWorkerInbound,
type ComputerWorkerOutbound,
} from "./protocol";
const START_TIMEOUT_MS = 10_000;
const CLOSE_TIMEOUT_MS = 1_500;
@@ -62,11 +68,11 @@ function wrapWorker(worker: Worker): ComputerWorkerHandle {
}
export function spawnComputerWorker(): ComputerWorkerHandle {
const processEntry =
process.env.PI_BUNDLED === "true" || process.env.PI_COMPILED === "true"
? new URL("./computer-worker-process-entry.js", import.meta.url).href
: new URL("../../computer-worker-process-entry.ts", import.meta.url).href;
return wrapWorker(new Worker(processEntry, { type: "module" }));
const hostEntry = workerHostEntry();
const worker = hostEntry
? new Worker(hostEntry, { type: "module", argv: [COMPUTER_WORKER_ARG] })
: new Worker(new URL("./worker-entry.ts", import.meta.url).href, { type: "module" });
return wrapWorker(worker);
}
interface PendingRequest {
@@ -1,6 +1,6 @@
import { parentPort } from "node:worker_threads";
import { consumeWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
import type { ComputerWorkerInbound, ComputerWorkerTransport } from "./protocol";
import { COMPUTER_WORKER_ARG, type ComputerWorkerInbound, type ComputerWorkerTransport } from "./protocol";
import { ComputerWorkerCore } from "./worker";
export function startComputerWorker(): void {
@@ -25,3 +25,10 @@ export function startComputerWorker(): void {
new ComputerWorkerCore(transport);
}
// Bun workers report `import.meta.main === false`. The source fallback still
// enters this file directly, while packaged CLI workers carry the selector and
// start the named entry only after installing its inbox.
if (!Bun.isMainThread && !process.argv.includes(COMPUTER_WORKER_ARG) && import.meta.path === Bun.main) {
startComputerWorker();
}
@@ -1,9 +1,5 @@
import {
type DesktopAction,
type DesktopCapture,
DesktopSession,
type DesktopSessionOptions,
} from "@oh-my-pi/pi-natives";
import type { DesktopAction, DesktopCapture, DesktopSession, DesktopSessionOptions } from "@oh-my-pi/pi-natives";
import { createDesktopSession } from "@oh-my-pi/pi-natives/desktop";
import type { ComputerWorkerError, ComputerWorkerInbound, ComputerWorkerTransport } from "./protocol";
export interface NativeDesktopSession {
@@ -44,7 +40,7 @@ export class ComputerWorkerCore {
constructor(
private readonly transport: ComputerWorkerTransport,
private readonly createSession: NativeDesktopSessionFactory = options => new DesktopSession(options),
private readonly createSession: NativeDesktopSessionFactory = createDesktopSession,
) {
this.#unsubscribe = transport.onMessage(message => this.#onMessage(message));
}
@@ -0,0 +1,9 @@
import { parentPort } from "node:worker_threads";
import { installWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
import { COMPUTER_WORKER_ARG } from "../../src/tools/computer/protocol";
import { startComputerWorker } from "../../src/tools/computer/worker-entry";
if (process.argv[2] !== COMPUTER_WORKER_ARG) throw new Error(`unknown worker selector: ${process.argv[2]}`);
if (!parentPort) throw new Error("computer worker fixture: missing parentPort");
installWorkerInbox(parentPort);
startComputerWorker();
@@ -1,10 +1,11 @@
const worker = new Worker(new URL("../../src/computer-worker-process-entry.ts", import.meta.url).href, {
const worker = new Worker(new URL("../../src/cli.ts", import.meta.url).href, {
type: "module",
argv: ["__omp_worker_computer"],
});
const response = Promise.withResolvers<unknown>();
worker.addEventListener("message", event => response.resolve(event.data));
worker.addEventListener("error", event => response.reject(event.error ?? new Error(event.message)));
worker.postMessage({ type: "ping", id: "computer-process-entry" });
worker.postMessage({ type: "ping", id: "computer-cli-selector" });
try {
process.stdout.write(`${JSON.stringify(await response.promise)}\n`);
} finally {
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Added
- Added the `@oh-my-pi/pi-natives/desktop` factory entry, which defers native addon loading until a desktop worker initializes its session.
## [17.1.3] - 2026-07-24
### Changed
+4
View File
@@ -0,0 +1,4 @@
import type { DesktopSession, DesktopSessionOptions } from "./index.js";
/** Construct a desktop session, loading the native addon on first use. */
export declare function createDesktopSession(options: DesktopSessionOptions): DesktopSession;
+10
View File
@@ -0,0 +1,10 @@
import { loadNative } from "./loader-state.js";
/**
* Construct a desktop session without loading the native addon until the
* computer worker receives its initialization message.
*/
export function createDesktopSession(options) {
const { DesktopSession } = loadNative();
return new DesktopSession(options);
}
+4
View File
@@ -62,6 +62,10 @@
".": {
"types": "./native/index.d.ts",
"import": "./native/index.js"
},
"./desktop": {
"types": "./native/desktop.d.ts",
"import": "./native/desktop.js"
}
}
}
+1 -4
View File
@@ -16,7 +16,6 @@ interface BinaryTarget {
const repoRoot = path.join(import.meta.dir, "..");
const binariesDir = path.join(repoRoot, "packages", "coding-agent", "binaries");
const entrypoint = path.join(repoRoot, "packages", "coding-agent", "src", "cli.ts");
const workerEntrypoint = path.join(repoRoot, "packages", "coding-agent", "src", "computer-worker-process-entry.ts");
const transformersManifest: unknown = createRequire(import.meta.url)("@huggingface/transformers/package.json");
if (
typeof transformersManifest !== "object" ||
@@ -27,8 +26,7 @@ if (
throw new Error("@huggingface/transformers package manifest has no string version");
}
const transformersVersion = transformersManifest.version;
// The computer worker is an independent compiled entry so its native graph is
// evaluated only when the supervisor selects it. Other workers re-enter the CLI.
// Worker threads re-enter the binary's single CLI host entry.
const isDryRun = process.argv.includes("--dry-run");
const targets: BinaryTarget[] = [
{
@@ -144,7 +142,6 @@ async function buildBinary(target: BinaryTarget): Promise<void> {
await compileCodingAgent({
repoRoot,
entrypoint,
workerEntrypoints: [workerEntrypoint],
outfile: path.join(repoRoot, target.outfile),
transformersVersion,
target: target.target,