Skip to content

fix(cron): prevent agent default model from overriding cron payload model#58294

Merged
steipete merged 3 commits intoopenclaw:mainfrom
aaronagent:fix/cron-model-override
Apr 4, 2026
Merged

fix(cron): prevent agent default model from overriding cron payload model#58294
steipete merged 3 commits intoopenclaw:mainfrom
aaronagent:fix/cron-model-override

Conversation

@aaronagent
Copy link
Copy Markdown
Contributor

Summary

  • Problem: When editing a cron job in the web UI and selecting a model via "Advanced", the cron execution ignores the override and uses the agent's default model instead ([Bug]: The specified model was designated for the cron, but the agent default model was always used during execution #58065).
  • Why it matters: Users who configure per-cron model overrides (e.g. cost-optimized models for scheduled tasks) always get billed for the more expensive agent default model, and get results from the wrong model.
  • What changed: When the cron payload carries an explicit model override, fallbacksOverride is now always a defined array (empty [] when no fallbacks are configured), which prevents runWithModelFallback from silently appending the agent's configured primary model as a last-resort fallback candidate.
  • What did NOT change (scope boundary): Cron jobs without a payload model override still get the full fallback chain including the agent primary model. The model resolution logic in resolveCronModelSelection is unchanged. Payload-level fallbacks arrays are still respected when present.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause / Regression History (if applicable)

  • Root cause: In src/cron/isolated-agent/run.ts, the fallbacksOverride parameter passed to runWithModelFallback was computed as payloadFallbacks ?? resolveAgentModelFallbacksOverride(params.cfg, agentId). When neither the cron payload fallbacks array nor the per-agent model fallbacks were configured (both undefined), fallbacksOverride resolved to undefined. Inside runWithModelFallbackresolveFallbackCandidates (model-fallback.ts:399), when fallbacksOverride === undefined, the agent's configured primary model is appended as the last fallback candidate. If the cron-selected model encounters any transient failure (rate limit, model-not-found in registry, overloaded, etc.), the fallback chain would reach and succeed on the agent's primary model — making it appear as though the cron model override was completely ignored.
  • Missing detection / guardrail: No test verified that fallbacksOverride was a defined array (rather than undefined) when a cron payload model override was present.
  • Prior context: PR fix: handle LiveSessionModelSwitchError in cron isolated sessions #57972 fixed LiveSessionModelSwitchError handling in cron isolated sessions but did not address this separate path where the agent default silently wins through the fallback candidate list.
  • Why this regressed now: The fallback behavior has always existed in runWithModelFallback, but it became more visible with the recent model provider expansions (multi-provider setups with qwen/gemini/codex) where transient failures are more common across provider boundaries.
  • If unknown, what was ruled out: Model resolution in resolveCronModelSelection is correct — the payload model IS resolved and passed to runWithModelFallback as provider/model. The issue is exclusively in the fallback candidate list construction.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts
  • Scenario the test should lock in: When a cron payload has model set, runWithModelFallback must receive a defined fallbacksOverride array (not undefined), preventing the agent primary from being silently appended as a fallback candidate.
  • Why this is the smallest reliable guardrail: Directly asserts on the parameter passed to runWithModelFallback, which is the exact boundary where the bug manifests.
  • Existing test that already covers this (if any): run.cron-model-override.test.ts covers pre-run session persistence but did not verify the fallback candidate behavior.

User-visible / Behavior Changes

  • Cron jobs with a payload model override will no longer silently fall back to the agent's default model on transient failures. Instead, the run will fail with an error if the override model is unavailable, giving users clear feedback.

Diagram (if applicable)

Before:
[cron job: model=gemini] -> runWithModelFallback(fallbacksOverride=undefined)
  -> candidates: [gemini, ..., agent-default-opus]
  -> gemini rate-limited -> tries opus -> succeeds with wrong model

After:
[cron job: model=gemini] -> runWithModelFallback(fallbacksOverride=[])
  -> candidates: [gemini]
  -> gemini rate-limited -> no more candidates -> error (correct)

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: macOS
  • Runtime/container: Node 22+
  • Model/provider: Any multi-provider setup (e.g. qwen>gemini>codex)
  • Integration/channel (if any): Web UI cron editor

Steps

  1. Configure an agent with a default model (e.g. anthropic/claude-opus-4-6)
  2. Create a cron job, click "Advanced", select a different model (e.g. google/gemini-2.0-flash)
  3. Run the cron job

Expected

  • The cron job executes using google/gemini-2.0-flash

Actual

  • The cron job executes using anthropic/claude-opus-4-6 (the agent default)

Evidence

  • Failing test/log before + passing after
    • New test file: src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts (5 tests)
    • All 155 existing isolated-agent tests continue to pass

Human Verification (required)

  • Verified scenarios:
    • Cron payload with model override: fallbacksOverride is a defined array
    • Cron payload without model override: fallbacksOverride is undefined (existing behavior preserved)
    • Cron payload with both model and fallbacks: explicit fallbacks array is used
    • Model override is forwarded to runEmbeddedPiAgent via passthrough
  • Edge cases checked:
    • Empty payload.model string → treated as no override
    • Payload with fallbacks array → fallbacks used as-is
    • Agent-level fallbacks configured → used when payload model override is present
  • What you did not verify: Live multi-provider execution (no access to configured providers)

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Risks and Mitigations

  • Risk: Cron jobs with model overrides that previously "worked" by silently falling back will now fail with an error if the override model is unavailable.
    • Mitigation: This is the correct behavior — users should see an error when their chosen model is unavailable, not silently get results from a different model. Agent-level fallbacks are still used when configured.

🤖 Generated with Claude Code

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 31, 2026

Greptile Summary

This PR fixes a bug where cron jobs with an explicit model payload override would silently fall back to the agent's default model on any transient failure (rate limit, model-not-found, etc.), because fallbacksOverride was passed as undefined to runWithModelFallback, which caused resolveFallbackCandidates to append the agent primary model as a last-resort candidate.

The fix in run.ts is minimal and well-scoped: a new hasCronPayloadModelOverride guard ensures cronFallbacksOverride is always at least [] (never undefined) when the payload carries a model override, while all other code paths remain unchanged.

Key changes:

  • src/cron/isolated-agent/run.ts — adds a hasCronPayloadModelOverride flag and derives cronFallbacksOverride to be a defined array (at minimum []) when a payload model is present, passing it to runWithModelFallback instead of the raw payloadFallbacks ?? agentFallbacks expression that could evaluate to undefined
  • src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts — new test file with 5 targeted scenarios covering: model forwarding to runWithModelFallback, model forwarding to the embedded agent runner, the fallbacksOverride !== undefined guarantee, preservation of the no-override path, and explicit payload fallbacks passthrough
  • The key test assertion at line 168–172 checks toBeDefined() and Array.isArray() but not the exact value; since both mocked sources return undefined in that test, tightening to toEqual([]) would make the guard more precise (P2 suggestion only)

Confidence Score: 5/5

This PR is safe to merge; the fix is minimal, well-scoped, and the behavior change is intentional and correctly documented.

The fix is a single ternary expression change in run.ts with clear semantics: ensure fallbacksOverride is always a defined array when a payload model override is present. The root cause analysis is correct (confirmed by reading resolveFallbackCandidates in model-fallback.ts), and the unchanged path for jobs without a model override is explicitly preserved. The new test file covers all documented scenarios. The only remaining finding is a P2 style suggestion to tighten one assertion from toBeDefined() to toEqual([]), which does not affect merge safety.

No files require special attention; the change is narrowly confined to run.ts lines 486-504.

Important Files Changed

Filename Overview
src/cron/isolated-agent/run.ts Adds hasCronPayloadModelOverride guard to ensure fallbacksOverride is always a defined array when a payload model override is present, preventing silent fallback to the agent primary model.
src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts New test file with 5 scenarios locking in the fallback override forwarding behavior for cron payload model overrides.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts
Line: 168-172

Comment:
**Test assertion could be more precise**

The assertions here confirm that `fallbacksOverride` is defined and is an array, but since both `payloadFallbacks` and `resolveAgentModelFallbacksOverride` are mocked to return `undefined` in this test, the resulting array will be exactly `[]`. Asserting `toEqual([])` would lock in the exact value and catch any future regression where the array unexpectedly gains entries.

```suggestion
    expect(capturedFallbacksOverride).toEqual([]);
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(cron): prevent agent default model f..." | Re-trigger Greptile

Comment on lines +168 to +172
// (not undefined), which prevents the agent primary from being appended
// as a silent fallback.
expect(capturedFallbacksOverride).toBeDefined();
expect(Array.isArray(capturedFallbacksOverride)).toBe(true);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test assertion could be more precise

The assertions here confirm that fallbacksOverride is defined and is an array, but since both payloadFallbacks and resolveAgentModelFallbacksOverride are mocked to return undefined in this test, the resulting array will be exactly []. Asserting toEqual([]) would lock in the exact value and catch any future regression where the array unexpectedly gains entries.

Suggested change
// (not undefined), which prevents the agent primary from being appended
// as a silent fallback.
expect(capturedFallbacksOverride).toBeDefined();
expect(Array.isArray(capturedFallbacksOverride)).toBe(true);
});
expect(capturedFallbacksOverride).toEqual([]);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts
Line: 168-172

Comment:
**Test assertion could be more precise**

The assertions here confirm that `fallbacksOverride` is defined and is an array, but since both `payloadFallbacks` and `resolveAgentModelFallbacksOverride` are mocked to return `undefined` in this test, the resulting array will be exactly `[]`. Asserting `toEqual([])` would lock in the exact value and catch any future regression where the array unexpectedly gains entries.

```suggestion
    expect(capturedFallbacksOverride).toEqual([]);
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 670947d98c

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/cron/isolated-agent/run.ts Outdated
Comment on lines +490 to +492
const cronFallbacksOverride = hasCronPayloadModelOverride
? (payloadFallbacks ?? resolveAgentModelFallbacksOverride(params.cfg, agentId) ?? [])
: (payloadFallbacks ?? resolveAgentModelFallbacksOverride(params.cfg, agentId));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve default fallback chain for payload model overrides

When payload.model is set and payload.fallbacks is absent, this now computes fallbacksOverride from resolveAgentModelFallbacksOverride(...) ?? [], which ignores agents.defaults.model.fallbacks. In configs that rely on global default fallbacks (but have no per-agent override), runWithModelFallback now receives a defined empty override and skips all configured default fallbacks, so cron jobs fail on the first transient model error instead of trying the configured fallback chain. This is a behavior regression introduced by the new ?? [] path.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@steipete steipete left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Root cause checks out.

I traced this against src/cron/isolated-agent/run.ts and src/agents/model-fallback.ts: when a cron payload carries an explicit model override but neither payload.fallbacks nor agent-level fallbacks are configured, we currently pass fallbacksOverride: undefined into runWithModelFallback. That re-enables appending the agent primary as a last-resort candidate, which is exactly how the requested model gets silently replaced by the agent default.

This patch keeps the no-override path intact and adds the right focused regression coverage. Mergeable as the canonical fix for the silent-fallback variant.

Copy link
Copy Markdown
Contributor

@steipete steipete left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One regression risk here.

When a cron payload carries an explicit model override, this patch forces fallbacksOverride to [] whenever there is no per-agent override. That does stop the unwanted implicit primary append, but it also disables the normal agents.defaults.model.fallbacks chain entirely for cron model overrides.

That differs from the shared override semantics used elsewhere. In src/agents/agent-scope.ts:255, explicit/session model overrides still inherit the global default fallback chain when there is no per-agent override. With this patch, the equivalent cron path would fail after the requested model instead of trying the configured defaults.

Please preserve the existing default fallback chain for explicit cron model overrides and only prevent the extra implicit primary append. The new tests should also cover the case where agents.defaults.model.fallbacks is configured and no per-agent override exists.

aaronagent and others added 3 commits April 4, 2026 14:14
…odel (openclaw#58065)

When a cron job specifies a model override via the Advanced settings,
runWithModelFallback could silently fall back to the agent's configured
primary model. This happened because fallbacksOverride was undefined
when neither payload.fallbacks nor per-agent fallbacks were configured,
causing resolveFallbackCandidates to append the agent primary as a
last-resort candidate. A transient failure on the cron-selected model
(rate limit, model-not-found, etc.) would then succeed on the agent
default, making it appear as if the override was ignored entirely.

Fix: when the cron payload carries an explicit model override, ensure
fallbacksOverride is always a defined array (empty when no fallbacks
are configured) so the agent primary is never silently appended.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace toBeDefined() + toBeInstanceOf(Array) with toEqual([])
to catch regressions where the array unexpectedly gains entries.
Addresses review feedback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@steipete steipete force-pushed the fix/cron-model-override branch from 6d658e0 to 655e0f3 Compare April 4, 2026 13:16
Copy link
Copy Markdown
Contributor

@steipete steipete left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the overcorrection.

  • cron now uses the shared resolveEffectiveModelFallbacks(...) path, so explicit payload.model overrides still inherit configured default fallbacks
  • the hidden agent-primary append is still blocked for override runs
  • added regression coverage for both the empty-chain case and inherited default fallbacks

Scoped verification:

  • pnpm test src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts
  • pnpm test src/cron/isolated-agent/run.payload-fallbacks.test.ts
  • pnpm test src/agents/agent-scope.test.ts

Local pnpm check is currently blocked by unrelated tsgo failures in untouched OpenRouter files on latest main.

@steipete steipete merged commit 16e7e25 into openclaw:main Apr 4, 2026
24 of 31 checks passed
@steipete
Copy link
Copy Markdown
Contributor

steipete commented Apr 4, 2026

Landed. Thanks @aaronagent.

Source head:

Merge commit:

Notes:

  • switched the cron path to the shared resolveEffectiveModelFallbacks(...) policy
  • kept the no-hidden-primary behavior for explicit payload.model runs
  • added regression coverage for empty and inherited fallback chains
  • local pnpm check stayed blocked by unrelated tsgo failures in untouched OpenRouter files on latest main, so this was landed with admin override after scoped verification

lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
…odel (openclaw#58294)

* fix(cron): prevent agent default model from overriding cron payload model (openclaw#58065)

When a cron job specifies a model override via the Advanced settings,
runWithModelFallback could silently fall back to the agent's configured
primary model. This happened because fallbacksOverride was undefined
when neither payload.fallbacks nor per-agent fallbacks were configured,
causing resolveFallbackCandidates to append the agent primary as a
last-resort candidate. A transient failure on the cron-selected model
(rate limit, model-not-found, etc.) would then succeed on the agent
default, making it appear as if the override was ignored entirely.

Fix: when the cron payload carries an explicit model override, ensure
fallbacksOverride is always a defined array (empty when no fallbacks
are configured) so the agent primary is never silently appended.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: use stricter toEqual([]) assertion for fallbacksOverride

Replace toBeDefined() + toBeInstanceOf(Array) with toEqual([])
to catch regressions where the array unexpectedly gains entries.
Addresses review feedback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: preserve cron override fallback semantics (openclaw#58294)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…odel (openclaw#58294)

* fix(cron): prevent agent default model from overriding cron payload model (openclaw#58065)

When a cron job specifies a model override via the Advanced settings,
runWithModelFallback could silently fall back to the agent's configured
primary model. This happened because fallbacksOverride was undefined
when neither payload.fallbacks nor per-agent fallbacks were configured,
causing resolveFallbackCandidates to append the agent primary as a
last-resort candidate. A transient failure on the cron-selected model
(rate limit, model-not-found, etc.) would then succeed on the agent
default, making it appear as if the override was ignored entirely.

Fix: when the cron payload carries an explicit model override, ensure
fallbacksOverride is always a defined array (empty when no fallbacks
are configured) so the agent primary is never silently appended.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test: use stricter toEqual([]) assertion for fallbacksOverride

Replace toBeDefined() + toBeInstanceOf(Array) with toEqual([])
to catch regressions where the array unexpectedly gains entries.
Addresses review feedback.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: preserve cron override fallback semantics (openclaw#58294)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: The specified model was designated for the cron, but the agent default model was always used during execution

2 participants