fix(cron): route CLI-runtime cron models through compatible backend (#76319)

Summary:
- The PR routes isolated cron executions through compatible configured CLI runtimes, threads agent identity into cron model selection, adds cron regression coverage, and records a changelog fix.
- Reproducibility: yes. The source PR describes cron jobs for agents with agentRuntime.id="claude-cli" selecti ... howing those canonical Anthropic attempts execute through claude-cli while OpenAI overrides stay on OpenAI.

ClawSweeper fixups:
- Included follow-up commit: fix(cron): route CLI-runtime cron models through compatible backend
- Included follow-up commit: fix(clawsweeper): address review for automerge-openclaw-openclaw-7584…
- Ran the ClawSweeper repair loop before final review.

Validation:
- ClawSweeper review passed for head ba2781de8f.
- Required merge gates passed before the squash merge.

Prepared head SHA: ba2781de8f
Review: https://github.com/openclaw/openclaw/pull/76319#issuecomment-4364991459

Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Co-authored-by: vishutdhar <68405187+vishutdhar@users.noreply.github.com>
This commit is contained in:
clawsweeper[bot]
2026-05-03 00:31:31 +00:00
committed by GitHub
parent 06cdb17ad2
commit 004e871656
6 changed files with 155 additions and 3 deletions

View File

@@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai
- Gateway: avoid repeated plugin tool descriptor config hashing so large runtime configs do not block reply startup and trigger reconnect/timeouts. (#75944) Thanks @joshavant.
- Plugins/externalization: keep diagnostics ClawHub packages and persisted bundled-plugin relocation on npm-first install metadata for launch, and omit Discord from the core package now that its external package is published. Thanks @vincentkoc.
- Setup/TUI: bound the Terminal hatch bootstrap run so a stalled provider request times out instead of leaving first-run hatching stuck behind the watchdog. (#76241) Thanks @joshavant.
- Cron/CLI runtimes: route isolated cron jobs through configured per-agent CLI runtimes only when the resolved model provider is compatible, so OpenAI job overrides no longer inherit a mismatched Claude CLI backend. Thanks @vishutdhar.
- Plugins/Codex: allow the official npm Codex plugin to install without the unsafe-install override, keep `/codex` command ownership, and cover the real npm Docker live path through managed `.openclaw/npm` dependencies plus uninstall failure proof.
- Gateway/status: add concrete service, config, listener-owner, and log collection next steps when gateway probes fail and Bonjour finds no local gateway, so frozen or port-conflict reports include the data needed for root-cause triage. Refs #49012. Thanks @vincentkoc.
- Codex harness: forward OpenClaw workspace bootstrap files such as `SOUL.md` through native Codex config instructions while leaving `AGENTS.md` to Codex project-doc discovery. Fixes #76273. Thanks @zknicker.

View File

@@ -65,6 +65,7 @@ type SelectModelOptions = {
providerOverride?: string;
};
isGmailHook?: boolean;
agentId?: string;
};
function parseModelRef(raw: string): { provider: string; model: string } | { error: string } {
@@ -126,6 +127,7 @@ async function selectModel(options: SelectModelOptions = {}) {
sessionEntry: options.sessionEntry ?? {},
payload: options.payload ?? defaultPayload(),
isGmailHook: options.isGmailHook ?? false,
agentId: options.agentId,
});
}
@@ -401,6 +403,94 @@ describe("cron model formatting and precedence edge cases", () => {
});
});
describe("CLI runtime compatibility", () => {
it("keeps the canonical Anthropic provider when a per-agent Claude CLI runtime is configured", async () => {
await expectSelectedModel(
{
cfg: {
agents: {
defaults: {
model: "anthropic/claude-opus-4-6",
},
list: [
{
id: "scheduler",
agentRuntime: { id: "claude-cli" },
},
],
},
},
agentId: "scheduler",
},
{ provider: "anthropic", model: "claude-opus-4-6" },
);
});
it("keeps an OpenAI payload override on OpenAI when per-agent Claude CLI is configured", async () => {
await expectSelectedModel(
{
cfg: {
agents: {
defaults: {
model: "anthropic/claude-opus-4-6",
},
list: [
{
id: "scheduler",
agentRuntime: { id: "claude-cli" },
},
],
},
},
agentId: "scheduler",
payload: {
kind: "agentTurn",
message: DEFAULT_MESSAGE,
model: "openai/gpt-4.1-mini",
},
},
{ provider: "openai", model: "gpt-4.1-mini" },
);
});
it("keeps the canonical Anthropic provider when a default Claude CLI runtime is configured", async () => {
await expectSelectedModel(
{
cfg: {
agents: {
defaults: {
model: "anthropic/claude-opus-4-6",
agentRuntime: { id: "claude-cli" },
},
},
},
},
{ provider: "anthropic", model: "claude-opus-4-6" },
);
});
it("keeps an OpenAI payload override on OpenAI when default Claude CLI is configured", async () => {
await expectSelectedModel(
{
cfg: {
agents: {
defaults: {
model: "anthropic/claude-opus-4-6",
agentRuntime: { id: "claude-cli" },
},
},
},
payload: {
kind: "agentTurn",
message: DEFAULT_MESSAGE,
model: "openai/gpt-4.1-mini",
},
},
{ provider: "openai", model: "gpt-4.1-mini" },
);
});
});
describe("stored session overrides", () => {
it("stored modelOverride/providerOverride are applied", async () => {
await expectSelectedModel(

View File

@@ -28,6 +28,7 @@ export type ResolveCronModelSelectionParams = {
sessionEntry: CronSessionModelOverrides;
payload: CronJob["payload"];
isGmailHook: boolean;
agentId?: string;
};
export type ResolveCronModelSelectionResult =

View File

@@ -1,3 +1,4 @@
import { resolveCliRuntimeExecutionProvider } from "../../agents/model-runtime-aliases.js";
import type { SkillSnapshot } from "../../agents/skills.js";
import { normalizeToolList } from "../../agents/tool-policy.js";
import type { ThinkLevel, VerboseLevel } from "../../auto-reply/thinking.js";
@@ -135,12 +136,18 @@ export function createCronPromptExecutor(params: {
if (params.abortSignal?.aborted) {
throw new Error(params.abortReason());
}
const executionProvider =
resolveCliRuntimeExecutionProvider({
provider: providerOverride,
cfg: params.cfgWithAgentDefaults,
agentId: params.agentId,
}) ?? providerOverride;
const bootstrapPromptWarningSignature =
bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1];
if (isCliProvider(providerOverride, params.cfgWithAgentDefaults)) {
if (isCliProvider(executionProvider, params.cfgWithAgentDefaults)) {
const cliSessionId = params.cronSession.isNewSession
? undefined
: await getCliSessionId(params.cronSession.sessionEntry, providerOverride);
: await getCliSessionId(params.cronSession.sessionEntry, executionProvider);
const result = await runCliAgent({
sessionId: params.cronSession.sessionEntry.sessionId,
sessionKey: params.runSessionKey,
@@ -151,7 +158,7 @@ export function createCronPromptExecutor(params: {
workspaceDir: params.workspaceDir,
config: params.cfgWithAgentDefaults,
prompt: promptText,
provider: providerOverride,
provider: executionProvider,
model: modelOverride,
thinkLevel: params.thinkLevel,
timeoutMs: params.timeoutMs,

View File

@@ -5,8 +5,11 @@ import {
setupRunCronIsolatedAgentTurnSuite,
} from "./run.suite-helpers.js";
import {
isCliProviderMock,
loadRunCronIsolatedAgentTurn,
resolveConfiguredModelRefMock,
resolveAgentModelFallbacksOverrideMock,
runCliAgentMock,
runWithModelFallbackMock,
} from "./run.test-harness.js";
@@ -54,4 +57,53 @@ describe("runCronIsolatedAgentTurn — payload.fallbacks", () => {
expect(runWithModelFallbackMock).toHaveBeenCalledOnce();
expect(runWithModelFallbackMock.mock.calls[0][0].fallbacksOverride).toEqual(expectedFallbacks);
});
it("plans Anthropic fallbacks canonically while executing compatible attempts through Claude CLI", async () => {
isCliProviderMock.mockImplementation((provider: string) => provider === "claude-cli");
resolveConfiguredModelRefMock.mockReturnValue({
provider: "anthropic",
model: "claude-opus-4-6",
});
runCliAgentMock.mockResolvedValue({
payloads: [{ text: "fallback ok" }],
meta: { agentMeta: { usage: { input: 10, output: 20 } } },
});
runWithModelFallbackMock.mockImplementation(async ({ provider, model, run }) => {
const firstResult = await run(provider, model);
const secondResult = await run("anthropic", "claude-sonnet-4-6");
return {
result: secondResult ?? firstResult,
provider: "anthropic",
model: "claude-sonnet-4-6",
attempts: [],
};
});
const result = await runCronIsolatedAgentTurn(
makeIsolatedAgentTurnParams({
cfg: {
agents: {
defaults: {
agentRuntime: { id: "claude-cli" },
model: {
primary: "anthropic/claude-opus-4-6",
fallbacks: ["anthropic/claude-sonnet-4-6"],
},
},
},
},
}),
);
expect(result.status).toBe("ok");
expect(runWithModelFallbackMock).toHaveBeenCalledOnce();
expect(runWithModelFallbackMock.mock.calls[0][0]).toMatchObject({
provider: "anthropic",
model: "claude-opus-4-6",
});
expect(runCliAgentMock.mock.calls.map((call) => [call[0].provider, call[0].model])).toEqual([
["claude-cli", "claude-opus-4-6"],
["claude-cli", "claude-sonnet-4-6"],
]);
});
});

View File

@@ -561,6 +561,7 @@ async function prepareCronRunContext(params: {
sessionEntry: cronSession.sessionEntry,
payload: input.job.payload,
isGmailHook,
agentId,
});
if (!resolvedModelSelection.ok) {
return {