fix(computer-use): re-enter single worker host
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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();
|
||||
+3
-2
@@ -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 {
|
||||
@@ -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
|
||||
|
||||
Vendored
+4
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
@@ -62,6 +62,10 @@
|
||||
".": {
|
||||
"types": "./native/index.d.ts",
|
||||
"import": "./native/index.js"
|
||||
},
|
||||
"./desktop": {
|
||||
"types": "./native/desktop.d.ts",
|
||||
"import": "./native/desktop.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user