diff --git a/server/internal/daemon/daemon.go b/server/internal/daemon/daemon.go index 010bd3027..5b0e79959 100644 --- a/server/internal/daemon/daemon.go +++ b/server/internal/daemon/daemon.go @@ -2439,6 +2439,34 @@ func (d *Daemon) runTask(ctx context.Context, task Task, provider string, slot i if task.Agent != nil { thinkingLevel = task.Agent.ThinkingLevel } + // Per-model guard: the server validates the literal token against the + // provider's enum, but per-model gaps (Claude's `xhigh` on a non-Opus + // model, Codex's per-model `supported_reasoning_levels`) only resolve + // here, against the daemon's local CLI catalog. Invalid combinations + // log a warning and drop the level rather than failing the task, so a + // stale persisted value never blocks execution. Empty model is passed + // through unchanged — ValidateThinkingLevel resolves it to the + // provider's default model internally so default-model tasks aren't + // misjudged. Discovery errors fail open: if we can't list models, we + // keep the persisted level and let the CLI surface any objection. + if thinkingLevel != "" { + ok, err := agent.ValidateThinkingLevel(ctx, provider, entry.Path, model, thinkingLevel) + if err != nil { + taskLog.Warn("thinking_level: catalog lookup failed; passing through", + "provider", provider, + "model", model, + "thinking_level", thinkingLevel, + "error", err, + ) + } else if !ok { + taskLog.Warn("thinking_level: not valid for this (provider, model); skipping injection", + "provider", provider, + "model", model, + "thinking_level", thinkingLevel, + ) + thinkingLevel = "" + } + } execOpts := agent.ExecOptions{ Cwd: env.WorkDir, Model: model,