fix(workers): isolate compiled host selectors
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 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.
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"]));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user