fix(codex): resolve managed package binary fallback

This commit is contained in:
Peter Steinberger
2026-05-02 18:00:10 +01:00
parent 03be4bfac5
commit 815665f839
3 changed files with 98 additions and 4 deletions

View File

@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Codex/app-server: resolve the managed `@openai/codex` package bin when package installs do not provide a nearby `.bin/codex` shim, avoiding false missing-binary startup failures.
- Plugins/source checkout: discover source-only plugins such as Codex from the `extensions/*` workspace while using npm package excludes as the packaged-core boundary, removing the stale core-bundle metadata path.
- Plugins/ClawHub: install ClawPack artifacts from the explicit npm-pack `.tgz` resolver path instead of the legacy ZIP-shaped placeholder route. Thanks @vincentkoc.
- Control UI: allow deployments to configure grouped chat message max-width with a validated `gateway.controlUi.chatMessageMaxWidth` setting instead of patching bundled CSS after upgrades. Fixes #67935. Thanks @xiew4589-lang.
@@ -25,7 +26,7 @@ Docs: https://docs.openclaw.ai
- CLI/update: treat inherited Gateway service markers as origin hints and only block package replacement when the managed Gateway is still live, so self-updates can stop the service and continue safely. (#75729) Thanks @hxy91819.
- Agents/failover: exempt run-level timeouts that fire during tool execution from model fallback, timeout-triggered compaction, and generic timeout payload synthesis. Long `process(poll)`, browser, or `exec` tool calls that exceed `agents.defaults.timeoutSeconds` previously rotated auth profiles, switched to a fallback model, and surfaced a misleading "LLM request timed out" error even though the primary model had already responded. Mirrors the existing `timedOutDuringCompaction` precedent (#46889). Fixes #52147. (#75873) Thanks @simonusa.
- Docker: copy Bun 1.3.13 from a digest-pinned image and keep CI on the same version. Fixes #74356. Thanks @fede-kamel and @sallyom.
- Agents/compaction: keep prior context on consecutive turns against z.ai-style providers (z.ai direct, openrouter z-ai/*, in-house GLM gateways); Pi's internal auto-compaction was misfiring after successful turns and clearing state.messages before the next provider request. (#76056) Thanks @openperf.
- Agents/compaction: keep prior context on consecutive turns against z.ai-style providers (z.ai direct, openrouter z-ai/\*, in-house GLM gateways); Pi's internal auto-compaction was misfiring after successful turns and clearing state.messages before the next provider request. (#76056) Thanks @openperf.
## 2026.5.2

View File

@@ -1,3 +1,5 @@
import { mkdir, mkdtemp, realpath, writeFile } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import type { CodexAppServerStartOptions } from "./config.js";
@@ -83,6 +85,39 @@ describe("managed Codex app-server binary", () => {
});
});
it("falls back to the resolved Codex package bin when no command shim exists", async () => {
const installRoot = await mkdtemp(path.join(os.tmpdir(), "openclaw-codex-package-"));
const pluginRoot = path.join(installRoot, "dist", "extensions", "codex");
const packageRoot = path.join(installRoot, "node_modules", "@openai", "codex");
const packageBin = path.join(packageRoot, "bin", "codex.js");
await mkdir(path.dirname(packageBin), { recursive: true });
await writeFile(
path.join(packageRoot, "package.json"),
JSON.stringify({
name: "@openai/codex",
bin: {
codex: "bin/codex.js",
},
}),
);
await writeFile(packageBin, "#!/usr/bin/env node\n");
const resolvedPackageBin = await realpath(packageBin);
const pathExists = vi.fn(async (filePath: string) => filePath === resolvedPackageBin);
await expect(
resolveManagedCodexAppServerStartOptions(startOptions("managed"), {
platform: "linux",
pluginRoot,
pathExists,
}),
).resolves.toEqual({
...startOptions("managed"),
command: resolvedPackageBin,
commandSource: "resolved-managed",
});
});
it("fails clearly when the managed Codex binary is missing", async () => {
await expect(
resolveManagedCodexAppServerStartOptions(startOptions("managed"), {

View File

@@ -1,5 +1,6 @@
import { constants as fsConstants } from "node:fs";
import { constants as fsConstants, readFileSync } from "node:fs";
import { access } from "node:fs/promises";
import { createRequire } from "node:module";
import path from "node:path";
import { fileURLToPath } from "node:url";
import type { CodexAppServerStartOptions } from "./config.js";
@@ -66,7 +67,21 @@ function resolveManagedCodexAppServerCommandCandidates(
): string[] {
const pathApi = pathForPlatform(platform);
const commandName = platform === "win32" ? "codex.cmd" : "codex";
const roots = [
const roots = resolveManagedCodexAppServerCandidateRoots(pluginRoot, platform);
return [
...new Set([
...roots.map((root) => pathApi.join(root, "node_modules", ".bin", commandName)),
...resolveManagedCodexPackageBinCandidates(roots, platform),
]),
];
}
function resolveManagedCodexAppServerCandidateRoots(
pluginRoot: string,
platform: NodeJS.Platform,
): string[] {
const pathApi = pathForPlatform(platform);
return [
pluginRoot,
pathApi.dirname(pluginRoot),
pathApi.dirname(pathApi.dirname(pluginRoot)),
@@ -74,7 +89,50 @@ function resolveManagedCodexAppServerCommandCandidates(
? pathApi.dirname(pathApi.dirname(pathApi.dirname(pluginRoot)))
: null,
].filter((root): root is string => Boolean(root));
return [...new Set(roots.map((root) => pathApi.join(root, "node_modules", ".bin", commandName)))];
}
function resolveManagedCodexPackageBinCandidates(
roots: readonly string[],
platform: NodeJS.Platform,
): string[] {
if (platform === "win32") {
return [];
}
const candidates: string[] = [];
for (const root of roots) {
const candidate = resolveManagedCodexPackageBinCandidate(root);
if (candidate) {
candidates.push(candidate);
}
}
return candidates;
}
function resolveManagedCodexPackageBinCandidate(root: string): string | null {
try {
const requireFromRoot = createRequire(path.join(root, "package.json"));
const packageJsonPath = requireFromRoot.resolve(
`${MANAGED_CODEX_APP_SERVER_PACKAGE}/package.json`,
);
const packageRoot = path.dirname(packageJsonPath);
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
bin?: unknown;
};
const binPath =
typeof packageJson.bin === "string"
? packageJson.bin
: isRecord(packageJson.bin) && typeof packageJson.bin.codex === "string"
? packageJson.bin.codex
: null;
return binPath ? path.resolve(packageRoot, binPath) : null;
} catch {
return null;
}
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isDistExtensionRoot(pluginRoot: string, platform: NodeJS.Platform): boolean {