Skip to content

Commit 004e871

Browse files
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 ba2781d. - Required merge gates passed before the squash merge. Prepared head SHA: ba2781d Review: #76319 (comment) Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: vishutdhar <[email protected]>
1 parent 06cdb17 commit 004e871

6 files changed

Lines changed: 155 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai
6060
- Gateway: avoid repeated plugin tool descriptor config hashing so large runtime configs do not block reply startup and trigger reconnect/timeouts. (#75944) Thanks @joshavant.
6161
- 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.
6262
- 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.
63+
- 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.
6364
- 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.
6465
- 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.
6566
- 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.

src/cron/isolated-agent.model-formatting.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ type SelectModelOptions = {
6565
providerOverride?: string;
6666
};
6767
isGmailHook?: boolean;
68+
agentId?: string;
6869
};
6970

7071
function parseModelRef(raw: string): { provider: string; model: string } | { error: string } {
@@ -126,6 +127,7 @@ async function selectModel(options: SelectModelOptions = {}) {
126127
sessionEntry: options.sessionEntry ?? {},
127128
payload: options.payload ?? defaultPayload(),
128129
isGmailHook: options.isGmailHook ?? false,
130+
agentId: options.agentId,
129131
});
130132
}
131133

@@ -401,6 +403,94 @@ describe("cron model formatting and precedence edge cases", () => {
401403
});
402404
});
403405

406+
describe("CLI runtime compatibility", () => {
407+
it("keeps the canonical Anthropic provider when a per-agent Claude CLI runtime is configured", async () => {
408+
await expectSelectedModel(
409+
{
410+
cfg: {
411+
agents: {
412+
defaults: {
413+
model: "anthropic/claude-opus-4-6",
414+
},
415+
list: [
416+
{
417+
id: "scheduler",
418+
agentRuntime: { id: "claude-cli" },
419+
},
420+
],
421+
},
422+
},
423+
agentId: "scheduler",
424+
},
425+
{ provider: "anthropic", model: "claude-opus-4-6" },
426+
);
427+
});
428+
429+
it("keeps an OpenAI payload override on OpenAI when per-agent Claude CLI is configured", async () => {
430+
await expectSelectedModel(
431+
{
432+
cfg: {
433+
agents: {
434+
defaults: {
435+
model: "anthropic/claude-opus-4-6",
436+
},
437+
list: [
438+
{
439+
id: "scheduler",
440+
agentRuntime: { id: "claude-cli" },
441+
},
442+
],
443+
},
444+
},
445+
agentId: "scheduler",
446+
payload: {
447+
kind: "agentTurn",
448+
message: DEFAULT_MESSAGE,
449+
model: "openai/gpt-4.1-mini",
450+
},
451+
},
452+
{ provider: "openai", model: "gpt-4.1-mini" },
453+
);
454+
});
455+
456+
it("keeps the canonical Anthropic provider when a default Claude CLI runtime is configured", async () => {
457+
await expectSelectedModel(
458+
{
459+
cfg: {
460+
agents: {
461+
defaults: {
462+
model: "anthropic/claude-opus-4-6",
463+
agentRuntime: { id: "claude-cli" },
464+
},
465+
},
466+
},
467+
},
468+
{ provider: "anthropic", model: "claude-opus-4-6" },
469+
);
470+
});
471+
472+
it("keeps an OpenAI payload override on OpenAI when default Claude CLI is configured", async () => {
473+
await expectSelectedModel(
474+
{
475+
cfg: {
476+
agents: {
477+
defaults: {
478+
model: "anthropic/claude-opus-4-6",
479+
agentRuntime: { id: "claude-cli" },
480+
},
481+
},
482+
},
483+
payload: {
484+
kind: "agentTurn",
485+
message: DEFAULT_MESSAGE,
486+
model: "openai/gpt-4.1-mini",
487+
},
488+
},
489+
{ provider: "openai", model: "gpt-4.1-mini" },
490+
);
491+
});
492+
});
493+
404494
describe("stored session overrides", () => {
405495
it("stored modelOverride/providerOverride are applied", async () => {
406496
await expectSelectedModel(

src/cron/isolated-agent/model-selection.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export type ResolveCronModelSelectionParams = {
2828
sessionEntry: CronSessionModelOverrides;
2929
payload: CronJob["payload"];
3030
isGmailHook: boolean;
31+
agentId?: string;
3132
};
3233

3334
export type ResolveCronModelSelectionResult =

src/cron/isolated-agent/run-executor.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { resolveCliRuntimeExecutionProvider } from "../../agents/model-runtime-aliases.js";
12
import type { SkillSnapshot } from "../../agents/skills.js";
23
import { normalizeToolList } from "../../agents/tool-policy.js";
34
import type { ThinkLevel, VerboseLevel } from "../../auto-reply/thinking.js";
@@ -135,12 +136,18 @@ export function createCronPromptExecutor(params: {
135136
if (params.abortSignal?.aborted) {
136137
throw new Error(params.abortReason());
137138
}
139+
const executionProvider =
140+
resolveCliRuntimeExecutionProvider({
141+
provider: providerOverride,
142+
cfg: params.cfgWithAgentDefaults,
143+
agentId: params.agentId,
144+
}) ?? providerOverride;
138145
const bootstrapPromptWarningSignature =
139146
bootstrapPromptWarningSignaturesSeen[bootstrapPromptWarningSignaturesSeen.length - 1];
140-
if (isCliProvider(providerOverride, params.cfgWithAgentDefaults)) {
147+
if (isCliProvider(executionProvider, params.cfgWithAgentDefaults)) {
141148
const cliSessionId = params.cronSession.isNewSession
142149
? undefined
143-
: await getCliSessionId(params.cronSession.sessionEntry, providerOverride);
150+
: await getCliSessionId(params.cronSession.sessionEntry, executionProvider);
144151
const result = await runCliAgent({
145152
sessionId: params.cronSession.sessionEntry.sessionId,
146153
sessionKey: params.runSessionKey,
@@ -151,7 +158,7 @@ export function createCronPromptExecutor(params: {
151158
workspaceDir: params.workspaceDir,
152159
config: params.cfgWithAgentDefaults,
153160
prompt: promptText,
154-
provider: providerOverride,
161+
provider: executionProvider,
155162
model: modelOverride,
156163
thinkLevel: params.thinkLevel,
157164
timeoutMs: params.timeoutMs,

src/cron/isolated-agent/run.payload-fallbacks.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import {
55
setupRunCronIsolatedAgentTurnSuite,
66
} from "./run.suite-helpers.js";
77
import {
8+
isCliProviderMock,
89
loadRunCronIsolatedAgentTurn,
10+
resolveConfiguredModelRefMock,
911
resolveAgentModelFallbacksOverrideMock,
12+
runCliAgentMock,
1013
runWithModelFallbackMock,
1114
} from "./run.test-harness.js";
1215

@@ -54,4 +57,53 @@ describe("runCronIsolatedAgentTurn — payload.fallbacks", () => {
5457
expect(runWithModelFallbackMock).toHaveBeenCalledOnce();
5558
expect(runWithModelFallbackMock.mock.calls[0][0].fallbacksOverride).toEqual(expectedFallbacks);
5659
});
60+
61+
it("plans Anthropic fallbacks canonically while executing compatible attempts through Claude CLI", async () => {
62+
isCliProviderMock.mockImplementation((provider: string) => provider === "claude-cli");
63+
resolveConfiguredModelRefMock.mockReturnValue({
64+
provider: "anthropic",
65+
model: "claude-opus-4-6",
66+
});
67+
runCliAgentMock.mockResolvedValue({
68+
payloads: [{ text: "fallback ok" }],
69+
meta: { agentMeta: { usage: { input: 10, output: 20 } } },
70+
});
71+
runWithModelFallbackMock.mockImplementation(async ({ provider, model, run }) => {
72+
const firstResult = await run(provider, model);
73+
const secondResult = await run("anthropic", "claude-sonnet-4-6");
74+
return {
75+
result: secondResult ?? firstResult,
76+
provider: "anthropic",
77+
model: "claude-sonnet-4-6",
78+
attempts: [],
79+
};
80+
});
81+
82+
const result = await runCronIsolatedAgentTurn(
83+
makeIsolatedAgentTurnParams({
84+
cfg: {
85+
agents: {
86+
defaults: {
87+
agentRuntime: { id: "claude-cli" },
88+
model: {
89+
primary: "anthropic/claude-opus-4-6",
90+
fallbacks: ["anthropic/claude-sonnet-4-6"],
91+
},
92+
},
93+
},
94+
},
95+
}),
96+
);
97+
98+
expect(result.status).toBe("ok");
99+
expect(runWithModelFallbackMock).toHaveBeenCalledOnce();
100+
expect(runWithModelFallbackMock.mock.calls[0][0]).toMatchObject({
101+
provider: "anthropic",
102+
model: "claude-opus-4-6",
103+
});
104+
expect(runCliAgentMock.mock.calls.map((call) => [call[0].provider, call[0].model])).toEqual([
105+
["claude-cli", "claude-opus-4-6"],
106+
["claude-cli", "claude-sonnet-4-6"],
107+
]);
108+
});
57109
});

src/cron/isolated-agent/run.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ async function prepareCronRunContext(params: {
561561
sessionEntry: cronSession.sessionEntry,
562562
payload: input.job.payload,
563563
isGmailHook,
564+
agentId,
564565
});
565566
if (!resolvedModelSelection.ok) {
566567
return {

0 commit comments

Comments
 (0)