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 (
maxrequested on a harness with nomaxfalls toxhigh). - If nothing supported ranks below it, the lowest supported level is used
(
lowrequested against a model that only supportshigh/maxbecomeshigh). - 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.
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:
decideEscalation() (src/lib/agents/tier-escalation.ts) walks this order
(D9):
- No effective difficulty →
block. - Promotions already at
max_promotions→block. - Already at
expert→block. - Attempts below
retries_at_tier→retry. effort_firstis on and this item has not raised effort yet →raise-effortone level above its current staffed effort (viasupportedEffortLevels(), so the raise never exceeds what the model/harness pair accepts) instead of promoting to the next tier’s model.- Otherwise →
promoteto the next tier’s model, exactly as beforeeffort_firstexisted.
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
effortwins on relaunch whatever its source. Aroles.<role>.effortconfig 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 (
maxon claude-code relaunched underohmypi) 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 callsPOST /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 422effort-rejected. In both cases the stored effort is unchanged. - A confirmed change is stored as
explicit: on the conversation row, or in the agent’sstate.json, where relaunch pins it.
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,getOhmypiLauncherFieldsinsrc/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), andpan handoff/pan fork --effort(default: inherit the source conversation’s effort). pi’soff/minimalare stored aslow. - 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>— itemmetadata.effort→ planmetadata.effort→roles.worker.effort→high; only claude-code and codex panes get a mapped--effort/model_reasoning_effortflag, every other harness launches with no effort flag (PAN-4256).pan flywheel start—roles.flywheel.effortat the next start; a paused Flywheel resumed keeps the effort it started with (PAN-4256).POST /api/agents(an expliciteffortfield in the body) andPOST /api/agents/:id/restart(ditto, pinning the relaunch asexplicit) (PAN-4256).pan start --remotework 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 throughresolveStaffing(), which resolves effort via the same precedence chain with the item’s tier as layer 4, andpickLaunchEffort()(src/lib/agents/launch-effort.ts) lets that staffed value win overpan start’s non-explicit default while still letting an explicit--effortoutrank it. - The standing tier supervisor (
spawnTierSupervisor) and the merge-train’s re-review dispatch (reDispatchVerificationinsrc/lib/cloister/merge-train-deps.ts), both viaresolveEffort({ role: 'review', ... }). - Planning —
pan plan --effort, the dashboard Plan dialog (defaulthigh, all five levels), and the planning half ofpan starton an unplanned issue. The start-planning API rejects an unknown level with 400; the planner resolves with roleplan(roles.plan.effort→ project →high) and is clamped to the planning model and harness. The planning prompt’s effort section names the resolved level, andhighand above add the probe pass. pan strike(roles.strike.effort→ project →high);--dry-runprints the resolved level and its source.- Recording — every new
sessions.jsonline carries the launcheffort, 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 asunrecorded. - Reporting —
pan cost effort [--issue <id>] [--json]groups spend by effort, one bucket per level plusunrecorded. - Display — Agents page History rows and the detail header show
<level> (<source>);pan statusprintsEffort:;pan showprintseffortand includeseffort/effortSourcein--json. The conversation composer chip shows it for conversations.