fix: share agent harness runtime activation (#67474)

This commit is contained in:
Peter Steinberger
2026-04-16 16:57:34 +01:00
parent f4bbd0122a
commit 86f108401b
6 changed files with 88 additions and 71 deletions

View File

@@ -42,6 +42,7 @@ Docs: https://docs.openclaw.ai
- Extensions/lmstudio: add exponential backoff to the inference-preload wrapper so an LM Studio model-load failure (for example the built-in memory guardrail rejecting a load because the swap is saturated) no longer produces a WARN line every ~2s for every chat request. The wrapper now records consecutive preload failures per `(baseUrl, modelKey, contextLength)` tuple with a 5s → 10s → 20s → … → 5min cooldown and skips the preload step entirely while a cooldown is active, letting chat requests proceed directly to the stream (the model is often already loaded via the LM Studio UI). The combined `preload failed` log line now reports consecutive-failure count and remaining cooldown so operators can act on the real issue instead of drowning in repeated warnings. (#67401) Thanks @xantorres.
- Agents/replay: re-run tool/result pairing after strict replay tool-call ID sanitization on outbound requests so Anthropic-compatible providers like MiniMax no longer receive malformed orphan tool-result IDs such as `...toolresult1` during compaction and retry flows. (#67620) Thanks @stainlu.
- Gateway/startup: fix spurious SIGUSR1 restart loop on Linux/systemd when plugin auto-enable is the only startup config write; the config hash guard was not captured for that write path, causing chokidar to treat each boot write as an external change and trigger a reload → restart cycle that corrupts manifest.db after repeated cycles. Fixes #67436. (#67557) thanks @openperf
- Codex/harness: auto-enable the Codex plugin when `codex` is selected as an embedded agent harness runtime, including forced default, per-agent, and `OPENCLAW_AGENT_RUNTIME` paths. (#67474) Thanks @duqaXxX.
- OpenAI Codex/CLI: keep resumed `codex exec resume` runs on the safe non-interactive path without reintroducing the removed dangerous bypass flag by passing the supported `--skip-git-repo-check` resume arg that real Codex CLI requires outside trusted git directories. (#67666) Thanks @plgonzalezrx8.
- Codex/app-server: parse Desktop-originated app-server user agents such as `Codex Desktop/0.118.0`, keeping the version gate working when the Codex CLI inherits a multi-word originator. (#64666) Thanks @cyrusaf.
- Cron/announce delivery: keep isolated announce `NO_REPLY` stripping case-insensitive across direct and text delivery, preserve structured media-only sends when a caption strips silent, and derive main-session awareness from the cleaned payloads so silent captions no longer leak stale `NO_REPLY` text. (#65016) Thanks @BKF-Gitty.

View File

@@ -0,0 +1,33 @@
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
import { isRecord } from "../utils.js";
export function collectConfiguredAgentHarnessRuntimes(
config: OpenClawConfig,
env: NodeJS.ProcessEnv,
): string[] {
const runtimes = new Set<string>();
const pushRuntime = (value: unknown) => {
if (typeof value !== "string") {
return;
}
const normalized = normalizeOptionalLowercaseString(value);
if (!normalized || normalized === "auto" || normalized === "pi") {
return;
}
runtimes.add(normalized);
};
pushRuntime(config.agents?.defaults?.embeddedHarness?.runtime);
if (Array.isArray(config.agents?.list)) {
for (const agent of config.agents.list) {
if (!isRecord(agent)) {
continue;
}
pushRuntime((agent.embeddedHarness as Record<string, unknown> | undefined)?.runtime);
}
}
pushRuntime(env.OPENCLAW_AGENT_RUNTIME);
return [...runtimes].toSorted((left, right) => left.localeCompare(right));
}

View File

@@ -247,6 +247,27 @@ describe("applyPluginAutoEnable core", () => {
);
});
it("auto-enables an opt-in plugin when an agent harness runtime is forced by env", () => {
const result = applyPluginAutoEnable({
config: {},
env: makeIsolatedEnv({ OPENCLAW_AGENT_RUNTIME: "codex" }),
manifestRegistry: makeRegistry([
{
id: "codex",
channels: [],
activation: {
onAgentHarnesses: ["codex"],
},
},
]),
});
expect(result.config.plugins?.entries?.codex?.enabled).toBe(true);
expect(result.changes).toContain(
"codex agent harness runtime configured, enabled automatically.",
);
});
it("skips auto-enable work for configs without channel or plugin-owned surfaces", () => {
const result = applyPluginAutoEnable({
config: {

View File

@@ -1,3 +1,4 @@
import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtimes.js";
import { normalizeProviderId } from "../agents/provider-id.js";
import {
hasPotentialConfiguredChannels,
@@ -99,35 +100,8 @@ function extractProviderFromModelRef(value: string): string | null {
return normalizeProviderId(trimmed.slice(0, slash));
}
function collectEmbeddedHarnessRuntimes(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): string[] {
const runtimes = new Set<string>();
const pushRuntime = (value: unknown) => {
if (typeof value !== "string") {
return;
}
const normalized = normalizeOptionalLowercaseString(value);
if (!normalized || normalized === "auto" || normalized === "pi") {
return;
}
runtimes.add(normalized);
};
pushRuntime(cfg.agents?.defaults?.embeddedHarness?.runtime);
if (Array.isArray(cfg.agents?.list)) {
for (const agent of cfg.agents.list) {
if (!isRecord(agent)) {
continue;
}
pushRuntime((agent.embeddedHarness as Record<string, unknown> | undefined)?.runtime);
}
}
pushRuntime(env.OPENCLAW_AGENT_RUNTIME);
return [...runtimes].toSorted((left, right) => left.localeCompare(right));
}
function hasConfiguredEmbeddedHarnessRuntime(cfg: OpenClawConfig, env: NodeJS.ProcessEnv): boolean {
return collectEmbeddedHarnessRuntimes(cfg, env).length > 0;
return collectConfiguredAgentHarnessRuntimes(cfg, env).length > 0;
}
function resolveAgentHarnessOwnerPluginIds(
@@ -490,7 +464,7 @@ export function resolveConfiguredPluginAutoEnableCandidates(params: {
}
}
for (const runtime of collectEmbeddedHarnessRuntimes(params.config, params.env)) {
for (const runtime of collectConfiguredAgentHarnessRuntimes(params.config, params.env)) {
const pluginIds = resolveAgentHarnessOwnerPluginIds(params.registry, runtime);
for (const pluginId of pluginIds) {
changes.push({

View File

@@ -144,6 +144,7 @@ function createManifestRegistryFixture() {
function expectStartupPluginIds(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
expected: readonly string[];
}) {
expect(
@@ -153,7 +154,7 @@ function expectStartupPluginIds(params: {
? { activationSourceConfig: params.activationSourceConfig }
: {}),
workspaceDir: "/tmp",
env: process.env,
env: params.env ?? process.env,
}),
).toEqual(params.expected);
expect(loadPluginManifestRegistry).toHaveBeenCalled();
@@ -162,6 +163,7 @@ function expectStartupPluginIds(params: {
function expectStartupPluginIdsCase(params: {
config: OpenClawConfig;
activationSourceConfig?: OpenClawConfig;
env?: NodeJS.ProcessEnv;
expected: readonly string[];
}) {
expectStartupPluginIds(params);
@@ -278,7 +280,7 @@ function createStartupConfig(params: {
: {}),
},
}
: {}),
: {}),
} as OpenClawConfig;
}
@@ -421,6 +423,16 @@ describe("resolveGatewayStartupPluginIds", () => {
});
});
it("includes required agent harness owner plugins when env forces the runtime", () => {
expectStartupPluginIdsCase({
config: createStartupConfig({
enabledPluginIds: ["codex"],
}),
env: { OPENCLAW_AGENT_RUNTIME: "codex" },
expected: ["demo-channel", "browser", "codex"],
});
});
it("does not include required agent harness owner plugins when they are explicitly disabled", () => {
expectStartupPluginIdsCase({
config: {

View File

@@ -1,3 +1,4 @@
import { collectConfiguredAgentHarnessRuntimes } from "../agents/harness-runtimes.js";
import { listPotentialConfiguredChannelIds } from "../channels/config-presence.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import {
@@ -50,33 +51,6 @@ function dedupeSortedPluginIds(values: Iterable<string>): string[] {
return [...new Set(values)].toSorted((left, right) => left.localeCompare(right));
}
function collectRequestedAgentHarnessRuntimes(
config: OpenClawConfig,
env: NodeJS.ProcessEnv,
): string[] {
const runtimes = new Set<string>();
const pushRuntime = (value: unknown) => {
const normalized = typeof value === "string" ? normalizeOptionalLowercaseString(value) : null;
if (!normalized || normalized === "auto" || normalized === "pi") {
return;
}
runtimes.add(normalized);
};
pushRuntime(config.agents?.defaults?.embeddedHarness?.runtime);
if (Array.isArray(config.agents?.list)) {
for (const entry of config.agents.list) {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
continue;
}
pushRuntime((entry as { embeddedHarness?: { runtime?: string } }).embeddedHarness?.runtime);
}
}
pushRuntime(env.OPENCLAW_AGENT_RUNTIME);
return [...runtimes].toSorted((left, right) => left.localeCompare(right));
}
function normalizeChannelIds(channelIds: Iterable<string>): string[] {
return Array.from(
new Set(
@@ -300,19 +274,21 @@ export function resolveGatewayStartupPluginIds(params: {
config: params.activationSourceConfig ?? params.config,
});
const requiredAgentHarnessPluginIds = new Set(
collectRequestedAgentHarnessRuntimes(params.activationSourceConfig ?? params.config, params.env)
.flatMap((runtime) =>
resolveManifestActivationPluginIds({
trigger: {
kind: "agentHarness",
runtime,
},
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
cache: true,
}),
),
collectConfiguredAgentHarnessRuntimes(
params.activationSourceConfig ?? params.config,
params.env,
).flatMap((runtime) =>
resolveManifestActivationPluginIds({
trigger: {
kind: "agentHarness",
runtime,
},
config: params.config,
workspaceDir: params.workspaceDir,
env: params.env,
cache: true,
}),
),
);
const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config);
const explicitMemorySlotStartupPluginId = resolveExplicitMemorySlotStartupPluginId(