Skip to content

fix(cron): preserve model fallbacks when agent overrides primary#18210

Merged
steipete merged 1 commit intoopenclaw:mainfrom
mahsumaktas:fix/cron-agent-model-fallback-preservation
Feb 16, 2026
Merged

fix(cron): preserve model fallbacks when agent overrides primary#18210
steipete merged 1 commit intoopenclaw:mainfrom
mahsumaktas:fix/cron-agent-model-fallback-preservation

Conversation

@mahsumaktas
Copy link
Contributor

@mahsumaktas mahsumaktas commented Feb 16, 2026

Problem

When an agent config specifies model: { primary: "..." } without an explicit fallbacks array, the cron isolated-agent runner replaces the entire model object from agents.defaults—discarding the default fallbacks chain.

This causes cron jobs (and agent sessions) to have only two model candidates:

  1. The pinned model (e.g., gemini-3-flash-preview)
  2. The global primary as a final fallback (e.g., gpt-5.3-codex)

All intermediate fallback models (Opus, DeepSeek, Qwen, Kimi, etc.) are silently lost.

Reproduction

// agents.defaults.model
{ "primary": "codex", "fallbacks": ["opus", "flash", "deepseek", "kimi"] }

// Agent config
{ "model": { "primary": "codex" } }

// Cron job pins model
{ "model": "flash" }

Before fix: fallback candidates = [flash, codex] — 3 models skipped
After fix: fallback candidates = [flash, opus, deepseek, kimi, codex]

Impact

Any user with:

  • Multiple agents that override model.primary
  • Cron jobs with per-job model pins
  • A configured fallback chain in agents.defaults.model.fallbacks

...will have their fallback chain silently broken. When the pinned model hits a rate limit (429), the system skips directly to the global primary instead of trying intermediate fallbacks.

Fix

3-line change in src/cron/isolated-agent/run.ts: spread the existing agentCfg.model before applying the override, so fallbacks (and any other model config keys) survive when the agent only overrides primary.

// Before (replace)
agentCfg.model = { primary: overrideModel };

// After (merge)
const existingModel = agentCfg.model && typeof agentCfg.model === "object" ? agentCfg.model : {};
agentCfg.model = { ...existingModel, primary: overrideModel };

Agents can still explicitly override or clear fallbacks by providing their own fallbacks array (including fallbacks: [] to disable).

Tests

Added run.model-fallback-preservation.test.ts with 6 test cases covering:

  • String override preserves fallbacks ✅
  • Object override preserves fallbacks ✅
  • Explicit fallbacks override works ✅
  • Empty fallbacks array clears chain ✅
  • Undefined override leaves model untouched ✅
  • Missing defaults model handled gracefully ✅

All existing tests pass.

Greptile Summary

Targeted bug fix that changes the model override logic in the cron isolated-agent runner from a full replace to a shallow merge. Previously, when an agent config specified model: { primary: "..." } without fallbacks, the entire model object from agents.defaults was replaced — silently discarding the default fallback chain. The fix spreads existingModel before applying the override, so fallbacks survive when only primary is overridden.

  • src/cron/isolated-agent/run.ts: 4-line change (including comment) that spreads the existing model config before applying the agent's override, correctly preserving fallbacks and any other model keys from defaults.
  • run.model-fallback-preservation.test.ts: 6 well-structured test cases covering string override, object override, explicit fallback override, empty fallback clearing, undefined override, and missing defaults. Tests re-implement the merge logic locally rather than testing the actual exported function, which is a reasonable approach given the function's complex dependencies.

Confidence Score: 5/5

  • This PR is safe to merge — it's a minimal, well-scoped fix with correct semantics and good test coverage.
  • The change is a 4-line fix (including a comment) that uses a standard JavaScript spread pattern to merge instead of replace. The logic is straightforward, handles edge cases (non-object model, string override, object override), and doesn't mutate shared config objects. The fix correctly interacts with both the upstream Object.assign merge and the downstream resolveFallbackCandidates function that reads cfg.agents.defaults.model.fallbacks. Six test cases cover the key scenarios. No regressions or side effects identified.
  • No files require special attention.

Last reviewed commit: 7af50ca

(4/5) You can add custom instructions or style guidelines for the agent here!

When an agent config specifies `model: { primary: "..." }` without
an explicit `fallbacks` array, the existing code replaced the entire
model object from `agents.defaults`—discarding the default fallbacks.

This caused cron jobs (and agent sessions) to have only one model
candidate (the pinned model) plus the global primary as a final
fallback, skipping all intermediate fallback models.

The fix merges the agent model override into the existing defaults
model object using spread, so that keys like `fallbacks` survive
when the agent only overrides `primary`. Agents can still explicitly
override or clear fallbacks by providing their own `fallbacks` array.

Reproduction scenario:
- `agents.defaults.model = { primary: "codex", fallbacks: ["opus", "flash", "deepseek"] }`
- Agent config: `model: { primary: "codex" }`
- Cron job pins: `model: "flash"`
- Before fix: fallback candidates = [flash, codex] (3 models lost)
- After fix: fallback candidates = [flash, opus, deepseek, ..., codex]
@steipete steipete merged commit 0ee3480 into openclaw:main Feb 16, 2026
25 checks passed
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.

2 participants