fix(codex): remove dynamic tools profile option

This commit is contained in:
Kevin Lin
2026-05-09 21:34:33 -07:00
committed by GitHub
parent 9f028e9942
commit b79de62b3c
18 changed files with 293 additions and 63 deletions

View File

@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
- Dependencies: refresh workspace pins and patch targets, including ACPX `@agentclientprotocol/claude-agent-acp` `0.33.1`, Codex ACP `0.14.0`, Baileys `7.0.0-rc10`, Google GenAI `2.0.1`, OpenAI `6.37.0`, AWS SDK `3.1045.0`, Kysely `0.29.0`, Tlon skill `0.3.6`, Aimock `1.19.5`, and tsdown `0.22.0`.
- Agents/compaction: preserve scoped background exec/process session references across embedded compaction and after-turn runtime contexts without exposing sessions from unrelated scopes. Fixes #79284. (#79307) Thanks @TurboTheTurtle.
- CLI/onboarding: improve setup, onboarding, configure, and channel command wayfinding so terminal flows explain the next useful command instead of relying on terse setup labels.
- Agents/Codex: remove the configurable Codex dynamic-tools profile so Codex app-server always owns workspace, edit, patch, exec, process, and plan tools while OpenClaw integration tools remain available.
### Fixes

View File

@@ -1,4 +1,4 @@
31ba805dfaa665fe16735d7c2f3a5e9fb0c349aeb5e301881b5da65cee62b1f8 config-baseline.json
f10281b32feb151b97952fda0722bae46c66456783e82ccaa7f39f11306287fa config-baseline.json
5552e7871057e05d5699f90f536ce58e62d5b8cfc6020d3e7106be7915fed041 config-baseline.core.json
80f0f51caedf14dc2138d975b62852ff7c5cf085df1c734c9de279f5859a7eeb config-baseline.channel.json
3a08e5422ce1422d9e2b75feac7e44cdcd0c3dc1eea594f664bceec13cbe3f58 config-baseline.plugin.json
dba159f639977bb96d79f0b78de2c6de48d25ed6ba1590f55812affb7ca6e4b0 config-baseline.plugin.json

View File

@@ -67,6 +67,7 @@ Notes:
- Doctor includes a memory-search readiness check and can recommend `openclaw configure --section model` when embedding credentials are missing.
- Doctor warns when no command owner is configured. The command owner is the human operator account allowed to run owner-only commands and approve dangerous actions. DM pairing only lets someone talk to the bot; if you approved a sender before first-owner bootstrap existed, set `commands.ownerAllowFrom` explicitly.
- Doctor warns when Codex-mode agents are configured and personal Codex CLI assets exist in the operator's Codex home. Local Codex app-server launches use isolated per-agent homes, so use `openclaw migrate codex --dry-run` to inventory assets that should be promoted deliberately.
- Doctor removes retired `plugins.entries.codex.config.codexDynamicToolsProfile`; Codex app-server always keeps Codex-native workspace tools native.
- Doctor warns when skills allowed for the default agent are unavailable in the current runtime environment because bins, env vars, config, or OS requirements are missing. `doctor --fix` can disable those unavailable skills with `skills.entries.<skill>.enabled=false`; install/configure the missing requirement instead when you want to keep the skill active.
- If sandbox mode is enabled but Docker is unavailable, doctor reports a high-signal warning with remediation (`install Docker` or `openclaw config set agents.defaults.sandbox.mode off`).
- If legacy sandbox registry files (`~/.openclaw/sandbox/containers.json` or `~/.openclaw/sandbox/browsers.json`) are present, doctor reports them; `openclaw doctor --fix` migrates valid entries into sharded registry directories and quarantines invalid legacy files.

View File

@@ -223,6 +223,7 @@ That stages grounded durable candidates into the short-term dreaming store while
- `browser.profiles.*.driver: "extension"` → `"existing-session"`
- remove `browser.relayBindHost` (legacy extension relay setting)
- legacy `models.providers.*.api: "openai"` → `"openai-completions"` (gateway startup also skips providers whose `api` is set to a future or unknown enum value rather than failing closed)
- remove `plugins.entries.codex.config.codexDynamicToolsProfile`; Codex app-server always keeps Codex-native workspace tools native
Doctor warnings also include account-default guidance for multi-account channels:

View File

@@ -42,7 +42,6 @@ Supported top-level fields:
| -------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `discovery` | enabled | Model discovery settings for Codex app-server `model/list`. |
| `appServer` | managed stdio app-server | Transport, command, auth, approval, sandbox, and timeout settings. |
| `codexDynamicToolsProfile` | `"native-first"` | Use `"openclaw-compat"` to expose the full OpenClaw dynamic tool set to Codex app-server. |
| `codexDynamicToolsLoading` | `"searchable"` | Use `"direct"` to put OpenClaw dynamic tools directly in the initial Codex tool context. |
| `codexDynamicToolsExclude` | `[]` | Additional OpenClaw dynamic tool names to omit from Codex app-server turns. |
| `codexPlugins` | disabled | Native Codex plugin/app support for migrated source-installed curated plugins. See [Native Codex plugins](/plugins/codex-native-plugins). |
@@ -211,9 +210,8 @@ isolation on local launches.
## Dynamic tools
Codex dynamic tools default to the `native-first` profile and `searchable`
loading. In that mode, OpenClaw does not expose dynamic tools that duplicate
Codex-native workspace operations:
Codex dynamic tools default to `searchable` loading. OpenClaw does not expose
dynamic tools that duplicate Codex-native workspace operations:
- `read`
- `write`

View File

@@ -377,6 +377,128 @@ Get the thread id from the completed `/diagnostics` reply, `/codex binding`, or
For upload mechanics and runtime-level diagnostics boundaries, see
[Codex harness runtime](/plugins/codex-harness-runtime#codex-feedback-upload).
Auth is selected in this order:
1. An explicit OpenClaw Codex auth profile for the agent.
2. The app-server's existing account in that agent's Codex home.
3. For local stdio app-server launches only, `CODEX_API_KEY`, then
`OPENAI_API_KEY`, when no app-server account is present and OpenAI auth is
still required.
When OpenClaw sees a ChatGPT subscription-style Codex auth profile, it removes
`CODEX_API_KEY` and `OPENAI_API_KEY` from the spawned Codex child process. That
keeps Gateway-level API keys available for embeddings or direct OpenAI models
without making native Codex app-server turns bill through the API by accident.
Explicit Codex API-key profiles and local stdio env-key fallback use app-server
login instead of inherited child-process env. WebSocket app-server connections
do not receive Gateway env API-key fallback; use an explicit auth profile or the
remote app-server's own account.
If a deployment needs additional environment isolation, add those variables to
`appServer.clearEnv`:
```json5
{
plugins: {
entries: {
codex: {
enabled: true,
config: {
appServer: {
clearEnv: ["CODEX_API_KEY", "OPENAI_API_KEY"],
},
},
},
},
},
}
```
`appServer.clearEnv` only affects the spawned Codex app-server child process.
Codex dynamic tools default to `searchable` loading. OpenClaw does not expose
dynamic tools that duplicate Codex-native workspace operations: `read`, `write`,
`edit`, `apply_patch`, `exec`, `process`, and `update_plan`. Remaining OpenClaw
integration tools such as messaging, sessions, media, cron, browser, nodes,
gateway, `heartbeat_respond`, and `web_search` are available through Codex tool
search under the `openclaw` namespace, keeping the initial model context
smaller.
`sessions_yield` and message-tool-only source replies stay direct because those
are turn-control contracts. Heartbeat collaboration instructions tell Codex to
search for `heartbeat_respond` before ending a heartbeat turn when the tool is
not already loaded.
Set `codexDynamicToolsLoading: "direct"` only when connecting to a custom Codex
app-server that cannot search deferred dynamic tools or when debugging the full
tool payload.
Supported top-level Codex plugin fields:
| Field | Default | Meaning |
| -------------------------- | -------------- | ---------------------------------------------------------------------------------------- |
| `codexDynamicToolsLoading` | `"searchable"` | Use `"direct"` to put OpenClaw dynamic tools directly in the initial Codex tool context. |
| `codexDynamicToolsExclude` | `[]` | Additional OpenClaw dynamic tool names to omit from Codex app-server turns. |
| `codexPlugins` | disabled | Native Codex plugin/app support for migrated source-installed curated plugins. |
Supported `appServer` fields:
| Field | Default | Meaning |
| ----------------------------- | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `transport` | `"stdio"` | `"stdio"` spawns Codex; `"websocket"` connects to `url`. |
| `command` | managed Codex binary | Executable for stdio transport. Leave unset to use the managed binary; set it only for an explicit override. |
| `args` | `["app-server", "--listen", "stdio://"]` | Arguments for stdio transport. |
| `url` | unset | WebSocket app-server URL. |
| `authToken` | unset | Bearer token for WebSocket transport. |
| `headers` | `{}` | Extra WebSocket headers. |
| `clearEnv` | `[]` | Extra environment variable names removed from the spawned stdio app-server process after OpenClaw builds its inherited environment. `CODEX_HOME` and `HOME` are reserved for OpenClaw's per-agent Codex isolation on local launches. |
| `requestTimeoutMs` | `60000` | Timeout for app-server control-plane calls. |
| `turnCompletionIdleTimeoutMs` | `60000` | Quiet window after a turn-scoped Codex app-server request while OpenClaw waits for `turn/completed`. Raise this for slow post-tool or status-only synthesis phases. |
| `mode` | `"yolo"` unless local Codex requirements disallow YOLO | Preset for YOLO or guardian-reviewed execution. Local stdio requirements that omit `danger-full-access`, `never` approval, or the `user` reviewer make the implicit default guardian. |
| `approvalPolicy` | `"never"` or an allowed guardian approval policy | Native Codex approval policy sent to thread start/resume/turn. Guardian defaults prefer `"on-request"` when allowed. |
| `sandbox` | `"danger-full-access"` or an allowed guardian sandbox | Native Codex sandbox mode sent to thread start/resume. Guardian defaults prefer `"workspace-write"` when allowed, otherwise `"read-only"`. |
| `approvalsReviewer` | `"user"` or an allowed guardian reviewer | Use `"auto_review"` to let Codex review native approval prompts when allowed, otherwise `guardian_subagent` or `user`. `guardian_subagent` remains a legacy alias. |
| `serviceTier` | unset | Optional Codex app-server service tier. `"priority"` enables fast-mode routing, `"flex"` requests flex processing, `null` clears the override, and legacy `"fast"` is accepted as `"priority"`. |
OpenClaw-owned dynamic tool calls are bounded independently from
`appServer.requestTimeoutMs`: Codex `item/tool/call` requests use a 30 second
OpenClaw watchdog by default. A positive per-call `timeoutMs` argument extends
or shortens that specific tool budget. The `image_generate` tool also uses
`agents.defaults.imageGenerationModel.timeoutMs` when the tool call does not
provide its own timeout, and the media-understanding `image` tool uses
`tools.media.image.timeoutSeconds` or its 60 second media default. Dynamic tool
budgets are capped at 600000 ms. On timeout, OpenClaw aborts the tool signal
where supported and returns a failed dynamic-tool response to Codex so the turn
can continue instead of leaving the session in `processing`.
After OpenClaw responds to a Codex turn-scoped app-server request, the harness
also expects Codex to finish the native turn with `turn/completed`. If the
app-server goes quiet for `appServer.turnCompletionIdleTimeoutMs` after that
response, OpenClaw best-effort interrupts the Codex turn, records a diagnostic
timeout, and releases the OpenClaw session lane so follow-up chat messages are
not queued behind a stale native turn. Any non-terminal notification for the
same turn, including `rawResponseItem/completed`, disarms that short watchdog
because Codex has proven the turn is still alive; the longer terminal watchdog
continues to protect genuinely stuck turns. Timeout diagnostics include the
last app-server notification method and, for raw assistant response items, the
item type, role, id, and a bounded assistant text preview.
Environment overrides remain available for local testing:
- `OPENCLAW_CODEX_APP_SERVER_BIN`
- `OPENCLAW_CODEX_APP_SERVER_ARGS`
- `OPENCLAW_CODEX_APP_SERVER_MODE=yolo|guardian`
- `OPENCLAW_CODEX_APP_SERVER_APPROVAL_POLICY`
- `OPENCLAW_CODEX_APP_SERVER_SANDBOX`
`OPENCLAW_CODEX_APP_SERVER_BIN` bypasses the managed binary when
`appServer.command` is unset.
`OPENCLAW_CODEX_APP_SERVER_GUARDIAN=1` was removed. Use
`plugins.entries.codex.config.appServer.mode: "guardian"` instead, or
`OPENCLAW_CODEX_APP_SERVER_MODE=guardian` for one-off local testing. Config is
preferred for repeatable deployments because it keeps the plugin behavior in the
same reviewed file as the rest of the Codex harness setup.
## Native Codex plugins
Native Codex plugin support uses Codex app-server's own app and plugin

View File

@@ -0,0 +1,44 @@
import { describe, expect, it } from "vitest";
import { legacyConfigRules, normalizeCompatibilityConfig } from "./doctor-contract-api.js";
describe("codex doctor contract", () => {
it("reports the retired dynamic tools profile config key", () => {
expect(
legacyConfigRules[0]?.match({
codexDynamicToolsProfile: "openclaw-compat",
codexDynamicToolsLoading: "direct",
}),
).toBe(true);
expect(legacyConfigRules[0]?.match({ codexDynamicToolsLoading: "direct" })).toBe(false);
});
it("removes the retired dynamic tools profile without dropping other Codex config", () => {
const original = {
plugins: {
entries: {
codex: {
enabled: true,
config: {
codexDynamicToolsProfile: "openclaw-compat",
codexDynamicToolsLoading: "direct",
codexDynamicToolsExclude: ["custom_tool"],
appServer: { mode: "guardian" },
},
},
},
},
};
const result = normalizeCompatibilityConfig({ cfg: original });
expect(result.changes).toEqual([
"Removed retired plugins.entries.codex.config.codexDynamicToolsProfile; Codex app-server always keeps Codex-native workspace tools native.",
]);
expect(result.config.plugins?.entries?.codex?.config).toEqual({
codexDynamicToolsLoading: "direct",
codexDynamicToolsExclude: ["custom_tool"],
appServer: { mode: "guardian" },
});
expect(original.plugins.entries.codex.config).toHaveProperty("codexDynamicToolsProfile");
});
});

View File

@@ -1,5 +1,61 @@
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import type { DoctorSessionRouteStateOwner } from "openclaw/plugin-sdk/runtime-doctor";
type LegacyConfigRule = {
path: string[];
message: string;
match: (value: unknown) => boolean;
};
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function hasRetiredDynamicToolsProfile(value: unknown): boolean {
return Object.prototype.hasOwnProperty.call(asRecord(value) ?? {}, "codexDynamicToolsProfile");
}
export const legacyConfigRules: LegacyConfigRule[] = [
{
path: ["plugins", "entries", "codex", "config"],
message:
'plugins.entries.codex.config.codexDynamicToolsProfile is retired; Codex app-server always keeps Codex-native workspace tools native. Run "openclaw doctor --fix".',
match: hasRetiredDynamicToolsProfile,
},
];
export function normalizeCompatibilityConfig({ cfg }: { cfg: OpenClawConfig }): {
config: OpenClawConfig;
changes: string[];
} {
const rawEntry = asRecord(cfg.plugins?.entries?.codex);
const rawPluginConfig = asRecord(rawEntry?.config);
if (!rawPluginConfig || !hasRetiredDynamicToolsProfile(rawPluginConfig)) {
return { config: cfg, changes: [] };
}
const nextConfig = structuredClone(cfg) as OpenClawConfig & {
plugins?: Record<string, unknown>;
};
const nextPlugins = asRecord(nextConfig.plugins);
const nextEntries = asRecord(nextPlugins?.entries);
const nextEntry = asRecord(nextEntries?.codex);
const nextPluginConfig = asRecord(nextEntry?.config);
if (!nextPluginConfig) {
return { config: cfg, changes: [] };
}
delete nextPluginConfig.codexDynamicToolsProfile;
return {
config: nextConfig,
changes: [
"Removed retired plugins.entries.codex.config.codexDynamicToolsProfile; Codex app-server always keeps Codex-native workspace tools native.",
],
};
}
export const sessionRouteStateOwners: DoctorSessionRouteStateOwner[] = [
{
id: "codex",

View File

@@ -33,11 +33,6 @@
"type": "object",
"additionalProperties": false,
"properties": {
"codexDynamicToolsProfile": {
"type": "string",
"enum": ["native-first", "openclaw-compat"],
"default": "native-first"
},
"codexDynamicToolsLoading": {
"type": "string",
"enum": ["searchable", "direct"],
@@ -197,11 +192,6 @@
}
},
"uiHints": {
"codexDynamicToolsProfile": {
"label": "Dynamic Tools Profile",
"help": "Select which OpenClaw dynamic tools are exposed to Codex app-server. native-first omits tools Codex already owns.",
"advanced": true
},
"codexDynamicToolsLoading": {
"label": "Dynamic Tools Loading",
"help": "Use searchable to defer OpenClaw dynamic tools behind Codex tool search, or direct to expose them in the initial context.",

View File

@@ -453,18 +453,25 @@ allowed_sandbox_modes = ["read-only", "workspace-write"]
);
});
it("parses dynamic tool profile controls", () => {
it("parses dynamic tool controls", () => {
expect(
readCodexPluginConfig({
codexDynamicToolsLoading: "direct",
codexDynamicToolsExclude: ["custom_tool"],
}),
).toEqual({
codexDynamicToolsLoading: "direct",
codexDynamicToolsExclude: ["custom_tool"],
});
});
it("rejects the retired dynamic tool profile key", () => {
expect(
readCodexPluginConfig({
codexDynamicToolsProfile: "openclaw-compat",
codexDynamicToolsLoading: "direct",
codexDynamicToolsExclude: ["custom_tool"],
}),
).toMatchObject({
codexDynamicToolsProfile: "openclaw-compat",
codexDynamicToolsLoading: "direct",
codexDynamicToolsExclude: ["custom_tool"],
});
).toEqual({});
});
it("parses native Codex plugin policy without treating wildcard as supported config", () => {

View File

@@ -31,7 +31,6 @@ export type CodexAppServerEffectiveApprovalPolicy =
export type CodexAppServerSandboxMode = "read-only" | "workspace-write" | "danger-full-access";
type CodexAppServerApprovalsReviewer = "user" | "auto_review" | "guardian_subagent";
type CodexAppServerCommandSource = "managed" | "resolved-managed" | "config" | "env";
type CodexDynamicToolsProfile = "native-first" | "openclaw-compat";
export type CodexDynamicToolsLoading = "searchable" | "direct";
export type CodexPluginDestructivePolicy = boolean;
@@ -110,7 +109,6 @@ export type CodexAppServerRuntimeOptions = {
};
export type CodexPluginConfig = {
codexDynamicToolsProfile?: CodexDynamicToolsProfile;
codexDynamicToolsLoading?: CodexDynamicToolsLoading;
codexDynamicToolsExclude?: string[];
discovery?: {
@@ -194,7 +192,6 @@ const codexAppServerApprovalPolicySchema = z.enum([
]);
const codexAppServerSandboxSchema = z.enum(["read-only", "workspace-write", "danger-full-access"]);
const codexAppServerApprovalsReviewerSchema = z.enum(["user", "auto_review", "guardian_subagent"]);
const codexDynamicToolsProfileSchema = z.enum(["native-first", "openclaw-compat"]);
const codexDynamicToolsLoadingSchema = z.enum(["searchable", "direct"]);
const codexAppServerServiceTierSchema = z
.preprocess(
@@ -222,7 +219,6 @@ const codexPluginsConfigSchema = z
const codexPluginConfigSchema = z
.object({
codexDynamicToolsProfile: codexDynamicToolsProfileSchema.optional(),
codexDynamicToolsLoading: codexDynamicToolsLoadingSchema.optional(),
codexDynamicToolsExclude: z.array(z.string()).optional(),
discovery: z

View File

@@ -1,6 +1,6 @@
import type { CodexPluginConfig } from "./config.js";
export const CODEX_NATIVE_FIRST_DYNAMIC_TOOL_EXCLUDES = [
export const CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES = [
"read",
"write",
"edit",
@@ -20,16 +20,13 @@ export function normalizeCodexDynamicToolName(name: string): string {
return DYNAMIC_TOOL_NAME_ALIASES[normalized] ?? normalized;
}
export function applyCodexDynamicToolProfile<T extends { name: string }>(
export function filterCodexDynamicTools<T extends { name: string }>(
tools: T[],
config: Pick<CodexPluginConfig, "codexDynamicToolsProfile" | "codexDynamicToolsExclude">,
config: Pick<CodexPluginConfig, "codexDynamicToolsExclude">,
): T[] {
const excludes = new Set<string>();
const profile = config.codexDynamicToolsProfile ?? "native-first";
if (profile === "native-first") {
for (const name of CODEX_NATIVE_FIRST_DYNAMIC_TOOL_EXCLUDES) {
excludes.add(name);
}
for (const name of CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES) {
excludes.add(name);
}
for (const name of config.codexDynamicToolsExclude ?? []) {
const trimmed = normalizeCodexDynamicToolName(name);

View File

@@ -544,7 +544,7 @@ describe("runCodexAppServerAttempt", () => {
await fs.rm(tempDir, { recursive: true, force: true });
});
it("defaults Codex dynamic tools to the native-first profile", () => {
it("filters Codex-native dynamic tools from app-server tool exposure", () => {
const tools = [
"read",
"write",
@@ -559,7 +559,7 @@ describe("runCodexAppServerAttempt", () => {
"sessions_spawn",
].map((name) => ({ name }));
expect(__testing.applyCodexDynamicToolProfile(tools, {}).map((tool) => tool.name)).toEqual([
expect(__testing.filterCodexDynamicTools(tools, {}).map((tool) => tool.name)).toEqual([
"web_search",
"message",
"heartbeat_respond",
@@ -567,17 +567,16 @@ describe("runCodexAppServerAttempt", () => {
]);
});
it("allows Codex dynamic tool filtering to opt back into OpenClaw compatibility", () => {
it("applies additional Codex dynamic tool excludes without exposing Codex-native tools", () => {
const tools = ["read", "exec", "message", "custom_tool"].map((name) => ({ name }));
expect(
__testing
.applyCodexDynamicToolProfile(tools, {
codexDynamicToolsProfile: "openclaw-compat",
.filterCodexDynamicTools(tools, {
codexDynamicToolsExclude: ["custom_tool"],
})
.map((tool) => tool.name),
).toEqual(["read", "exec", "message"]);
).toEqual(["message"]);
});
it("starts Codex threads without duplicate OpenClaw workspace tools by default", async () => {
@@ -590,7 +589,7 @@ describe("runCodexAppServerAttempt", () => {
}
throw new Error(`unexpected method: ${method}`);
});
const dynamicTools = __testing.applyCodexDynamicToolProfile(
const dynamicTools = __testing.filterCodexDynamicTools(
[
"read",
"write",

View File

@@ -73,10 +73,7 @@ import {
type CodexPluginConfig,
} from "./config.js";
import { projectContextEngineAssemblyForCodex } from "./context-engine-projection.js";
import {
applyCodexDynamicToolProfile,
normalizeCodexDynamicToolName,
} from "./dynamic-tool-profile.js";
import { filterCodexDynamicTools, normalizeCodexDynamicToolName } from "./dynamic-tool-profile.js";
import { createCodexDynamicToolBridge, type CodexDynamicToolBridge } from "./dynamic-tools.js";
import { handleCodexAppServerElicitationRequest } from "./elicitation-bridge.js";
import { CodexAppServerEventProjector } from "./event-projector.js";
@@ -1907,8 +1904,8 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
input.runAbortController.abort("sessions_yield");
},
});
const profiledTools = applyCodexDynamicToolProfile(allTools, input.pluginConfig);
const visionFilteredTools = filterToolsForVisionInputs(profiledTools, {
const codexFilteredTools = filterCodexDynamicTools(allTools, input.pluginConfig);
const visionFilteredTools = filterToolsForVisionInputs(codexFilteredTools, {
modelHasVision,
hasInboundImages: (params.images?.length ?? 0) > 0,
});
@@ -2369,7 +2366,7 @@ export const __testing = {
CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS,
CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS,
buildCodexNativeHookRelayId,
applyCodexDynamicToolProfile,
filterCodexDynamicTools,
buildDynamicTools,
filterCodexDynamicToolsForAllowlist,
filterToolsForVisionInputs,

View File

@@ -7,7 +7,7 @@ import {
resolveCodexAppServerRuntimeOptions,
} from "./src/app-server/config.js";
import type { CodexPluginConfig } from "./src/app-server/config.js";
import { applyCodexDynamicToolProfile } from "./src/app-server/dynamic-tool-profile.js";
import { filterCodexDynamicTools } from "./src/app-server/dynamic-tool-profile.js";
import { createCodexDynamicToolBridge } from "./src/app-server/dynamic-tools.js";
import type { CodexDynamicToolSpec, JsonObject } from "./src/app-server/protocol.js";
import {
@@ -70,15 +70,12 @@ export function buildCodexHarnessPromptSnapshot(params: {
export function createCodexDynamicToolSpecsForPromptSnapshot(params: {
tools: AnyAgentTool[];
pluginConfig?: Pick<
CodexPluginConfig,
"codexDynamicToolsProfile" | "codexDynamicToolsLoading" | "codexDynamicToolsExclude"
>;
pluginConfig?: Pick<CodexPluginConfig, "codexDynamicToolsLoading" | "codexDynamicToolsExclude">;
directToolNames?: Iterable<string>;
}): CodexDynamicToolSpec[] {
const profiledTools = applyCodexDynamicToolProfile(params.tools, params.pluginConfig ?? {});
const filteredTools = filterCodexDynamicTools(params.tools, params.pluginConfig ?? {});
return createCodexDynamicToolBridge({
tools: profiledTools,
tools: filteredTools,
signal: new AbortController().signal,
loading: params.pluginConfig?.codexDynamicToolsLoading ?? "searchable",
directToolNames: params.directToolNames,

View File

@@ -19,6 +19,7 @@ const ANSI_ESCAPE_RE = new RegExp(String.raw`\u001B\[[0-9;]*m`, "g");
const HASHED_ROOT_JS_RE = /^(?<base>.+)-[A-Za-z0-9_-]+\.js$/u;
const DEFAULT_CAPTURE_BYTES = 8 * 1024 * 1024;
const DEFAULT_HEARTBEAT_MS = 30_000;
const DEFAULT_TSDOWN_NODE_OPTIONS = "--max-old-space-size=6144";
const TERMINATION_GRACE_MS = 5_000;
const TSDOWN_OUTPUT_ROOTS = ["dist", "dist-runtime"];
const GENERATED_SOURCE_DECLARATION_PATHSPEC = ":(glob)extensions/**/*.d.ts";
@@ -199,6 +200,19 @@ function parseNonNegativeInteger(value) {
return Math.trunc(parsed);
}
function resolveTsdownEnv(env) {
const nodeOptions = env.NODE_OPTIONS?.trim() ?? "";
if (/(?:^|\s)--max-old-space-size(?:=|\s+)/u.test(nodeOptions)) {
return env;
}
return {
...env,
NODE_OPTIONS: nodeOptions
? `${nodeOptions} ${DEFAULT_TSDOWN_NODE_OPTIONS}`
: DEFAULT_TSDOWN_NODE_OPTIONS,
};
}
export function createTsdownOutputScanner(params = {}) {
const maxCaptureBytes = params.maxCaptureBytes ?? DEFAULT_CAPTURE_BYTES;
let captured = "";
@@ -242,7 +256,7 @@ export function createTsdownOutputScanner(params = {}) {
}
export function resolveTsdownBuildInvocation(params = {}) {
const env = params.env ?? process.env;
const env = resolveTsdownEnv(params.env ?? process.env);
const runner = resolvePnpmRunner({
pnpmArgs: [
"exec",

View File

@@ -82,7 +82,10 @@ type CodexPromptSnapshotApi = {
};
createCodexDynamicToolSpecsForPromptSnapshot: (params: {
tools: AnyAgentTool[];
pluginConfig?: { codexDynamicToolsProfile?: "native-first" | "openclaw-compat" };
pluginConfig?: {
codexDynamicToolsLoading?: "searchable" | "direct";
codexDynamicToolsExclude?: string[];
};
directToolNames?: string[];
}) => CodexDynamicToolSpec[];
};
@@ -359,7 +362,6 @@ function createDynamicTools(params: {
});
return codexApi.createCodexDynamicToolSpecsForPromptSnapshot({
tools: normalized.filter((tool) => HAPPY_PATH_TOOL_NAMES.has(tool.name)),
pluginConfig: { codexDynamicToolsProfile: "native-first" },
directToolNames: ["message"],
});
}
@@ -670,9 +672,7 @@ function renderScenarioSnapshot(scenario: PromptScenario): string {
scenario,
sessionKey: scenario.ctx.SessionKey ?? `agent:main:${scenario.id}`,
});
const appServer = codexApi.resolveCodexPromptSnapshotAppServerOptions({
codexDynamicToolsProfile: "native-first",
});
const appServer = codexApi.resolveCodexPromptSnapshotAppServerOptions();
const codexSnapshot = codexApi.buildCodexHarnessPromptSnapshot({
attempt,
cwd: WORKSPACE_DIR,

View File

@@ -54,11 +54,21 @@ describe("resolveTsdownBuildInvocation", () => {
stdio: ["ignore", "pipe", "pipe"],
shell: false,
windowsVerbatimArguments: undefined,
env: {},
env: { NODE_OPTIONS: "--max-old-space-size=6144" },
},
});
});
it("preserves explicit tsdown heap settings", () => {
const result = resolveTsdownBuildInvocation({
nodeExecPath: "/usr/bin/node",
npmExecPath: "/tmp/pnpm.cjs",
env: { NODE_OPTIONS: "--trace-warnings --max-old-space-size=8192" },
});
expect(result.options.env.NODE_OPTIONS).toBe("--trace-warnings --max-old-space-size=8192");
});
it("keeps source-checkout prune best-effort", () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
const rmSync = vi.spyOn(fs, "rmSync");