Skip to main content

Reasoning effort

Reasoning effort is the --effort level a coding-agent harness launches at (Claude Code’s low/medium/high/xhigh/max, and the nearest equivalent on every other harness). Overdeck resolves it through one function, resolveEffort() (src/lib/agents/resolve-effort.ts), on top of one enum, EFFORT_LEVELS (packages/contracts/src/effort.ts) — instead of the ad hoc effort ?? 'high' fallbacks and copied 'low' | 'medium' | 'high' unions scattered through earlier code.

Levels

xhigh was added in Opus 4.7 (between high and max); max predates it (Opus 4.6+/Sonnet 4.6). The default is high — nothing resolves to xhigh or max unless something explicitly asked for it.

Where effort comes from

resolveEffort() walks a fixed precedence chain and returns the first layer that supplies a valid level, plus which layer won: An invalid explicit value throws InvalidEffortError. An invalid item, plan, or project value is skipped silently (a project value also adds a warning) and resolution falls through to the next layer — configuration typos degrade rather than crash a spawn.

Config keys

Clamping

Once a level is resolved, clampEffort() checks it against what the target model and harness actually support (supportedEffortLevels() intersects the model’s effortLevels from model-capabilities.ts with the harness’s effortLevels from harness-behavior.ts; either side being unrestricted contributes all five levels):
  • If the resolved level is supported, it’s used as-is.
  • If not, the highest supported level below it is used instead (max requested on a harness with no max falls to xhigh).
  • If nothing supported ranks below it, the lowest supported level is used (low requested against a model that only supports high/max becomes high).
  • If the model and harness restrictions don’t overlap at all, the requested level is kept as-is and flagged with a warning rather than dropped.
Pi (ohmypi) and Muse Code harness rows stop at xhigh, so max clamps to xhigh for them even though their CLIs parse max. Kimi K3 lists all five levels and translates them at launch (medium→high, xhigh→max). See How each harness receives effort. Config validation rejects; a resolved flag clamps. Setting an unsupported level in config.yaml (roles.<role>.effort, roles.<role>.sub.<name>.effort, or tiered_execution.tiers.<name>.effort) fails config load with effortConfigErrors()’s is not supported by <model> message — you have to fix the file. An explicit --effort flag or a resolved runtime value that turns out unsupported is clamped instead, with a warning printed to stderr — a spawn never hard-fails over it.

Tier escalation

tiered_execution.escalation.effort_first raises an item’s effort one level before promoting it to the next tier’s model:
For an item that has exhausted its retries at the current tier, decideEscalation() (src/lib/agents/tier-escalation.ts) walks this order (D9):
  1. No effective difficulty → block.
  2. Promotions already at max_promotions → block.
  3. Already at expert → block.
  4. Attempts below retries_at_tier → retry.
  5. effort_first is on and this item has not raised effort yet → raise-effort one level above its current staffed effort (via supportedEffortLevels(), so the raise never exceeds what the model/harness pair accepts) instead of promoting to the next tier’s model.
  6. Otherwise → promote to the next tier’s model, exactly as before effort_first existed.
Only one effort raise happens per tier — once effectiveEffort is set on the item’s override, the next escalation falls through to step 6 and promotes. A raise does not count toward max_promotions: applyEscalationAction() leaves promotions unchanged for a raise-effort action, only incrementing it for a promote. When there is no supported level above the current effort (already at max, or the model/harness pair caps below the next level), decideEscalation() promotes instead of raising. decideEscalation() and applyEscalationAction() are pure functions with no caller yet — no supervisor-blocked or verification-failed event wires a real escalation trigger. That connection is tracked in #4512. In Settings, each tiered-execution crew has its own Effort select (aria-label="Crew effort", inherit or one of the five levels), the Escalation panel has a “Raise effort before promoting the model” switch for effort_first, and each review sub-role under Roles Models has its own Effort select.

Relaunch keeps effort

state.json stores the launched effort and its effortSource (PAN-4253). Every relaunch path — resume, restart, crash recovery, the messaging fresh-launch fallback, session rotation, crash respawn, and cloister model handoff — goes through one helper, resolveRelaunchEffort() (src/lib/agents/relaunch-effort.ts):
  • A persisted effort wins on relaunch whatever its source. A roles.<role>.effort config change reaches new spawns only; it does not retroactively change an agent that already has a pinned effort.
  • The persisted level is always re-clamped to the relaunch’s model and harness, so a model or harness switch (max on claude-code relaunched under ohmypi) clamps down and the clamped level is what gets stored back — never the original request.
  • A legacy or effort-less agent resolves through the normal role → project → default chain on its first relaunch, and that resolved value is then pinned for every relaunch after it.

Changing effort on a running session

Pick a level in the composer’s effort picker while a session runs. For a claude-code conversation the picker calls POST /api/conversations/:name/thinking-level; for an agent-backed panel (a work agent or any other claude-code agent) it calls POST /api/agents/:id/effort. Both take { "level": "<level>" }. Claude Code has no control channel, so Overdeck types /effort <level> into the session’s pane. It then waits up to 10 seconds for Claude Code’s Set effort level to <level> (…) confirmation in the session transcript, and stores the level only after that confirmation arrives:
  • The level is first clamped to the model, the same as at launch.
  • A session that is mid-turn is refused (409 busy), because Claude Code queues typed input during a turn and the change could land after the window. A pending permission prompt or a subagent holding the input is also refused (409), and nothing is typed.
  • No confirmation within 10 seconds answers 504 not-confirmed; a rejected level answers 422 effort-rejected. In both cases the stored effort is unchanged.
  • A confirmed change is stored as explicit: on the conversation row, or in the agent’s state.json, where relaunch pins it.
Claude Code itself saves low, medium, high, and xhigh as the default for new sessions of that model in ~/.claude/settings.json (max is session-only). Overdeck does not change that file. Its own sessions always launch with --effort, so they are not affected, but your own interactive claude sessions of that model start at the saved level. The effort chip. For a running session the picker reads <Level> · <source>, for example High · default or Low · set. The source is the layer the level came from: set (an explicit choice), item, plan, tier, sub-role, role, project, or default. terminal means the session transcript shows a different level than Overdeck launched or stored, so someone ran /effort in the native terminal. The chip’s tooltip says whether the level was observed, that is read from the session transcript (claude-code 2.1.280 and later write the effort on every assistant turn).

What honors it today

  • pan start <id> for work-agent spawns.
  • Definition-less role runs (review sub-roles, the standing supervisor) and their codex/omp launcher fields (getRoleRuntimeBaseCommand, getCodexLauncherFields, getOhmypiLauncherFields in src/lib/agents/runtime-command.ts).
  • Every relaunch path: resume (resumeAgent), restart (restartAgent), crash recovery (recoverAgent), the messaging fresh-launch fallback, session rotation, crash respawn, and cloister model handoff — see Relaunch keeps effort.
  • Conversations — dashboard creation (sidebar and Home composer effort pickers, and the New conversation with options… dialog, whose default and its source come from GET /api/effort/default?model=&harness=&issue=), resume, restart-all, switch-model (clamped to the new model), and pan handoff/pan fork --effort (default: inherit the source conversation’s effort). pi’s off/minimal are stored as low.
  • Live change on a running session — claude-code conversations and claude-code agents through /effort, and codex, ACP, OpenCode, and pi conversations through their control channels — see Changing effort on a running session.
  • Every harness adapter — codex (TUI and app-server), OpenCode and Kimi via ACP, native Kimi Code, Pi and Muse — receives a resolved, clamped level from its producer and never applies its own default (see How each harness receives effort).
  • The review parent’s fresh spawn — roles.review.effort (the resumed-session branch keeps the saved session’s effort unchanged) (PAN-4256).
  • The test role’s dispatch — both the automatic post-review queue and the manual re-dispatch route — roles.test.effort (PAN-4256).
  • pan worker run --effort <level> (PAN-4256).
  • pan spawn --effort <level> — item metadata.effort → plan metadata.effort → roles.worker.effort → high; only claude-code and codex panes get a mapped --effort/model_reasoning_effort flag, every other harness launches with no effort flag (PAN-4256).
  • pan flywheel start — roles.flywheel.effort at the next start; a paused Flywheel resumed keeps the effort it started with (PAN-4256).
  • POST /api/agents (an explicit effort field in the body) and POST /api/agents/:id/restart (ditto, pinning the relaunch as explicit) (PAN-4256).
  • pan start --remote work agents on Fly — roles.work.effort (PAN-4256).
  • Tiered-execution work spawns — a registered swarm slot (resolveSlotTierSpawnParams) and the single work agent (resolveSingleWorkTierSpawnParams) both staff through resolveStaffing(), which resolves effort via the same precedence chain with the item’s tier as layer 4, and pickLaunchEffort() (src/lib/agents/launch-effort.ts) lets that staffed value win over pan start’s non-explicit default while still letting an explicit --effort outrank it.
  • The standing tier supervisor (spawnTierSupervisor) and the merge-train’s re-review dispatch (reDispatchVerification in src/lib/cloister/merge-train-deps.ts), both via resolveEffort({ role: 'review', ... }).
  • Planning — pan plan --effort, the dashboard Plan dialog (default high, all five levels), and the planning half of pan start on an unplanned issue. The start-planning API rejects an unknown level with 400; the planner resolves with role plan (roles.plan.effort → project → high) and is clamped to the planning model and harness. The planning prompt’s effort section names the resolved level, and high and above add the probe pass.
  • pan strike (roles.strike.effort → project → high); --dry-run prints the resolved level and its source.
  • Recording — every new sessions.json line carries the launch effort, and every new cost-ledger row (cost_events.effort, events.jsonl) carries the effort its request ran at: for claude-code the level on the transcript record, else the session’s launch effort. Rows recorded before this change read as unrecorded.
  • Reporting — pan cost effort [--issue <id>] [--json] groups spend by effort, one bucket per level plus unrecorded.
  • Display — Agents page History rows and the detail header show <level> (<source>); pan status prints Effort:; pan show prints effort and includes effort/effortSource in --json. The conversation composer chip shows it for conversations.
Everything else — issue-view and conversation-row display — still resolves effort ad hoc and is tracked in sibling issues: