fix(workers): isolate compiled host selectors

This commit is contained in:
usr-bin-roygbiv
2026-07-25 08:31:54 +00:00
parent 8e1bffb60f
commit 9ceebb2a4d
9 changed files with 119 additions and 9 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 while packaged workers re-enter the single CLI host entry.
- 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, normal CLI startup keeps the native desktop worker graph lazy, and packaged workers re-enter the single CLI host without the computer module claiming non-computer selectors.
- 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.
+3 -3
View File
@@ -25,12 +25,13 @@ import {
VERSION,
} from "@oh-my-pi/pi-utils/dirs";
import { interceptUnhandledRejections } from "@oh-my-pi/pi-utils/postmortem";
import { declareWorkerHostEntry, installWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
import { declareWorkerHostEntry, installWorkerInbox, isWorkerHostSelector } from "@oh-my-pi/pi-utils/worker-host";
import { installProfileAlias, resolveProfileAliasCommandFromProcess } from "./cli/profile-alias";
import { extractProfileFlags } from "./cli/profile-bootstrap";
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 { COMPUTER_WORKER_ARG } from "./tools/computer/protocol";
import { smokeTestComputerWorker } from "./tools/computer/supervisor";
import { startComputerWorker } from "./tools/computer/worker-entry";
@@ -113,7 +114,6 @@ 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";
@@ -351,7 +351,7 @@ export async function runCli(argv: string[]): Promise<void> {
// synchronous prefix of `runWorkerEntrypoint`, and Bun flushes the
// worker's parked initial messages as soon as the entry module's
// top-level evaluation finishes.
if (resolvedArgv[0]?.startsWith("__omp_worker_")) {
if (isWorkerHostSelector(resolvedArgv[0])) {
const dispatched = await runWorkerEntrypoint(resolvedArgv[0]);
if (!dispatched) {
process.stderr.write(`Error: unknown worker selector: ${resolvedArgv[0]}\n`);
@@ -104,3 +104,34 @@ it("dispatches the computer worker from a single npm-style host bundle", async (
fs.rmSync(outDir, { recursive: true, force: true });
}
});
it("keeps non-computer selectors isolated in a compiled single-entry worker host", async () => {
using tempDir = TempDir.createSync("@omp-compiled-worker-selector-");
const packageDir = path.resolve(import.meta.dir, "../../..");
const outfile = path.join(tempDir.path(), process.platform === "win32" ? "worker-host.exe" : "worker-host");
const build = Bun.spawn(
[
process.execPath,
"build",
"--compile",
"--target=bun",
`--outfile=${outfile}`,
path.join(packageDir, "test/fixtures/compiled-worker-selector-host.ts"),
],
{ cwd: packageDir, stdout: "pipe", stderr: "pipe" },
);
const [buildExitCode, buildStderr] = await Promise.all([build.exited, new Response(build.stderr).text()]);
expect(buildExitCode, buildStderr).toBe(0);
const proc = Bun.spawn([outfile], {
cwd: packageDir,
stdout: "pipe",
stderr: "pipe",
});
const [exitCode, stdout, stderr] = await Promise.all([
proc.exited,
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
]);
expect(exitCode, stderr).toBe(0);
expect(stdout).toBe('{"ok":true,"kind":"pong"}\n');
});
@@ -1,6 +1,6 @@
import { parentPort } from "node:worker_threads";
import { consumeWorkerInbox } from "@oh-my-pi/pi-utils/worker-host";
import { COMPUTER_WORKER_ARG, type ComputerWorkerInbound, type ComputerWorkerTransport } from "./protocol";
import { consumeWorkerInbox, isWorkerHostSelector } from "@oh-my-pi/pi-utils/worker-host";
import type { ComputerWorkerInbound, ComputerWorkerTransport } from "./protocol";
import { ComputerWorkerCore } from "./worker";
export function startComputerWorker(): void {
@@ -29,6 +29,6 @@ export function startComputerWorker(): void {
// 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) {
if (!Bun.isMainThread && !process.argv.some(isWorkerHostSelector) && import.meta.path === Bun.main) {
startComputerWorker();
}
@@ -0,0 +1,36 @@
import { parentPort } from "node:worker_threads";
import { installWorkerInbox, WORKER_HOST_SELECTOR_PREFIX } 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";
const STATS_WORKER_ARG = `${WORKER_HOST_SELECTOR_PREFIX}stats_sync`;
declare const self: Worker & {
onmessage: ((event: MessageEvent<{ kind: "ping" }>) => void) | null;
};
if (Bun.isMainThread) {
const worker = new Worker(Bun.main, { type: "module", argv: [STATS_WORKER_ARG] });
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({ kind: "ping" });
try {
process.stdout.write(`${JSON.stringify(await response.promise)}\n`);
} finally {
worker.terminate();
}
} else {
const selector = process.argv.find(arg => arg.startsWith(WORKER_HOST_SELECTOR_PREFIX));
if (selector === COMPUTER_WORKER_ARG) {
if (!parentPort) throw new Error("compiled worker fixture: missing parentPort");
installWorkerInbox(parentPort);
startComputerWorker();
} else if (selector === STATS_WORKER_ARG) {
self.onmessage = (_event: MessageEvent<{ kind: "ping" }>) => {
self.postMessage({ ok: true, kind: "pong" });
};
} else {
throw new Error(`unknown worker selector: ${selector}`);
}
}
+8
View File
@@ -1,5 +1,13 @@
import { stripWindowsExtendedLengthPathPrefix } from "./path";
/** Prefix reserved for argv selectors dispatched by the shared CLI worker host. */
export const WORKER_HOST_SELECTOR_PREFIX = "__omp_worker_";
/** Whether an argv value selects a worker hosted by the shared CLI entrypoint. */
export function isWorkerHostSelector(value: string | undefined): value is string {
return value?.startsWith(WORKER_HOST_SELECTOR_PREFIX) ?? false;
}
/**
* Main-module path declared by self-dispatching CLI entrypoints — entries
* whose top-level argv handling routes hidden `__omp_*` worker selectors.
+16 -1
View File
@@ -1,6 +1,11 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
import { EventEmitter } from "node:events";
import { consumeWorkerInbox, installWorkerInbox } from "../src/worker-host";
import {
consumeWorkerInbox,
installWorkerInbox,
isWorkerHostSelector,
WORKER_HOST_SELECTOR_PREFIX,
} from "../src/worker-host";
/**
* Regression for JS/tab eval workers always stalling until the init timeout.
@@ -14,6 +19,16 @@ import { consumeWorkerInbox, installWorkerInbox } from "../src/worker-host";
* buffers until the worker module `bind`s the real handler; these tests pin that
* buffer-replay contract.
*/
describe("worker-host selectors", () => {
it("recognizes the shared selector namespace without claiming ordinary CLI arguments", () => {
expect(WORKER_HOST_SELECTOR_PREFIX).toBe("__omp_worker_");
expect(isWorkerHostSelector("__omp_worker_stats_sync")).toBeTrue();
expect(isWorkerHostSelector("__omp_worker_computer")).toBeTrue();
expect(isWorkerHostSelector("--version")).toBeFalse();
expect(isWorkerHostSelector(undefined)).toBeFalse();
});
});
describe("worker-host inbox", () => {
// State is a module-global stash (one worker per process); drain it around
// each test so nothing leaks into the next.
+19 -1
View File
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { $ } from "bun";
import { inspectPackedTarball, isVersionAlreadyPublished } from "./ci-release-publish.ts";
import { inspectPackedTarball, isVersionAlreadyPublished, prepareNativeCorePackage } from "./ci-release-publish.ts";
const temporaryDirectories: string[] = [];
@@ -40,4 +40,22 @@ describe("release publish", () => {
expect(isVersionAlreadyPublished("You cannot publish over the previously published versions: 1.2.3.")).toBe(true);
expect(isVersionAlreadyPublished("cannot publish over the previously published version")).toBe(false);
});
it("ships every file required by the lazy desktop export in the native core", async () => {
const root = await fs.mkdtemp(path.join(os.tmpdir(), "omp-native-core-publish-test-"));
temporaryDirectories.push(root);
await Bun.write(
path.join(root, "package.json"),
JSON.stringify({
name: "@oh-my-pi/pi-natives",
version: "1.2.3",
exports: {
"./desktop": { types: "./native/desktop.d.ts", import: "./native/desktop.js" },
},
}),
);
const manifest = await prepareNativeCorePackage(root, false);
expect(manifest.files).toEqual(expect.arrayContaining(["native/desktop.js", "native/desktop.d.ts"]));
});
});
+2
View File
@@ -201,6 +201,8 @@ export async function prepareNativeCorePackage(pkgDir: string, write: boolean):
manifest.files = [
"native/index.js",
"native/index.d.ts",
"native/desktop.js",
"native/desktop.d.ts",
"native/loader-state.js",
"native/loader-state.d.ts",
"native/embedded-addon.js",