Skip to content

Per-session reasoning effort for Claude and Codex - #164

Merged
PleasePrompto merged 1 commit into
PleasePrompto:mainfrom
ryuhaneul:feat/claude-effort-session
Jul 12, 2026
Merged

Per-session reasoning effort for Claude and Codex#164
PleasePrompto merged 1 commit into
PleasePrompto:mainfrom
ryuhaneul:feat/claude-effort-session

Conversation

@ryuhaneul

@ryuhaneul ryuhaneul commented Jun 25, 2026

Copy link
Copy Markdown
Contributor

Summary

This brings reasoning effort to parity with model selection: effort is resolved per session — each forum topic can run at its own effort, and the main chat / DM sets the global default — for both providers (Claude --effort, Codex -c model_reasoning_effort).

The two providers started from very different places, so this is a larger change than "make a setting per-session":

  • Claude had no reasoning-effort support at all. claude_provider never emitted --effort and ignored reasoning_effort entirely. This PR adds Claude reasoning effort for the first time — the --effort flag, the /effort command + selector, provider-aware validation, and /status display.
  • Codex already consumed the global config.reasoning_effort (-c model_reasoning_effort), but only as a single service-wide default: there was no command to change it at runtime and no per-conversation scope, so changing effort never actually took effect in a given topic. This PR makes a topic's /effort apply to Codex in that topic.

After this change:

  • changing effort in a forum topic affects only that topic (Claude and Codex);
  • changing it in the main chat / DM updates the global default.

Starting point (v0.18.2)

  • config.reasoning_effort exists as one global value (default medium).
  • codex_provider reads it → service-wide only; no runtime command, no per-topic scope.
  • claude_provider does not read effort at all.
  • There is no /effort command and no per-session effort field (only model is per-session).

What changed

  • Claude providerclaude_provider now emits --effort <value> (skipped only when the value is default). First-class reasoning effort for Claude.
  • /effort command + selector — new command and inline selector in model_selector, offering the efforts valid for the active session's provider/model.
  • SessionData.reasoning_effort — new per-session field, captured when a session is created and preserved afterward, mirroring how model is captured (preserve_existing_target). A session keeps the effort it was created with instead of drifting when the global default later changes.
  • Per-turn resolution — a reasoning_effort_override is threaded through every turn path (normal flow, named foreground/background sessions, injection, task hub, webhook observer, cron) and resolved in _make_cli as effort = request.effort_override or config.reasoning_effort, exactly mirroring model_override or default_model. Both providers read the single resolved value, so Claude and Codex alike honor per-session effort.
  • Effort selection scope — effort is written to the global default only from the main chat / DM; in a topic it is stored on that topic's session. A provider switch re-validates the carried-over effort and resets an unsupported value.
  • Named / background carriersNamedSession, BackgroundSubmit, BackgroundTask, TaskSubmit, TaskEntry now carry reasoning_effort, so named foreground/background runs keep their session's effort.
  • /status — shows the effective effort (the session's value in a topic, otherwise the global default).
  • Effort selection response — picking an effort echoes the applied value (e.g. Reasoning effort set to high) across all locales instead of a generic "updated" line, matching how model selection confirms the chosen model.
  • cron / webhook executors emit the resolved effort as well.
  • Coupled /model alignment fixswitch_model shares its resolution path with per-session effort, and this PR also makes it compare the requested model against the active session's model in the main chat / DM, not only in topics (the main chat previously compared against config.model). This lets /model re-align a session whose model has drifted from config.model, and realigns config.provider to the new model's provider. It only has a visible effect when config.model has been changed out of band so that it differs from the active session — e.g. editing config.json directly, or the per-agent config hot-reload used to switch a sub-agent's model. Under normal in-chat /model use the active session and the global default always move together, so this is a no-op. Flagged explicitly as it is a /model behavior change rather than an effort one; it can be split into a separate PR on request.

Provider-aware validation

Effort values are validated against the selected provider's supported set — Claude low/medium/high/xhigh/max; Codex's set is discovered per model, falling back to low/medium/high/xhigh. An effort that isn't valid for a newly selected model/provider is reset rather than passed through.

Tests

New and extended coverage across the model selector, orchestrator core, flows, commands, named-session recovery, tasks, cron execution, the Claude provider, and the param resolver (the model-selector suite alone adds ~830 lines). Full suite is green with no new failures.

Note for merging alongside #156 (interactive REPL provider)

Both this PR and #156 modify the single per-request config builder CLIService._make_cli, so they conflict there. Effort is resolved once in _make_cli (effort = request.effort_override or config.reasoning_effort) and stored on the provider as self._config.reasoning_effort; every execution path reads it from there — the streaming path (_build_command) and #156's _send_interactive alike. When both land, resolve the _make_cli conflict by keeping reasoning_effort=effort (the per-session value) and adding #156's interactive_repl_pool / interactive_enabled fields. Note that _make_cli is shared by both paths, so keeping the global reasoning_effort=self._config.reasoning_effort instead would silently drop per-session effort everywhere.

A pooled interactive REPL (#156) is spawned once and reused, so it keeps the effort it was spawned with. #156 already drops the pooled REPL on a model change (switch_modelkill_interactive_repl); an effort change needs the same, otherwise a topic's REPL keeps answering at the old effort until it happens to respawn. This PR adds that drop in the effort handler through a small getattr-guarded helper — a no-op on this branch (which targets a base without #156) and an active REPL-respawn once #156 lands. Changing effort in a topic running interactive then respawns its REPL at the new effort immediately.

Supersedes #161, which added Claude reasoning effort but applied it globally.

Expose reasoning effort for Claude (--effort) with a /effort command,
provider-aware validation (claude low..max, codex low..xhigh with a no-cache
fallback), /status display, cron/webhook executor support, model-picker effort
sub-selector, and i18n — scoped per-session like model (replaces the closed
PleasePrompto#161, which made topic effort changes leak globally and broke codex).

Mirror the existing per-session model mechanism exactly:
- SessionData gains a reasoning_effort field; sync_session_target/resolve_session
  store and capture-on-first-use (fixed at creation time, like model).
- AgentRequest gains effort_override; CLIService resolves
  request.effort_override or config default (like model_override or default_model).
- flows (_prepare_normal, named, heartbeat), injection, tasks/hub, and the
  background observer thread the effective per-session effort into every turn.
- switch_model applies the global default only from main/DM (if not is_topic);
  in a topic it writes the topic session's effort, with provider-switch
  re-validation resetting an unsupported carried-over effort to medium.
- NamedSession/BackgroundSubmit/BackgroundTask/TaskSubmit/TaskEntry carry
  reasoning_effort so named foreground/background runs keep their session effort.
- claude_provider (--effort) and codex_provider (-c model_reasoning_effort) both
  consume the resolved per-turn effort.
- /status shows the effective effort (session in a topic, else global default).
@ryuhaneul
ryuhaneul force-pushed the feat/claude-effort-session branch from 4b28d8c to dccc9c9 Compare June 25, 2026 06:12
@PleasePrompto
PleasePrompto merged commit e2fc3d8 into PleasePrompto:main Jul 12, 2026
PleasePrompto added a commit that referenced this pull request Jul 12, 2026
- cron one-shot Claude command: gate --effort on the 'default' sentinel
  like the -p path, instead of silently dropping an explicit 'medium'
- extract TaskHub._prepare_request from _run (PLR0915 after the #157/#164
  merges pushed _run past 50 statements)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PleasePrompto added a commit that referenced this pull request Jul 12, 2026
Post-merge fixes threading the Slack transport through main's newer
subsystems (#164 per-session effort, #157 append_system_prompt_files,
#166 topic-scoped kills, #137/#138 task changes):

- Delete fork-only .github/workflows/upstream-release-watch.yml
- Remove dead _is_message_addressed() in messenger/slack/bot.py
- Fix trailing comma in config.example.json Slack section (invalid JSON)
- Drop fork-only "ductor-slack" brand strings from en/wizard.toml
- Harden _normalized_transport_list() in __main__.py: honor a non-empty
  transports list whenever the primary is a member (primary-first, others
  preserved) instead of collapsing to [primary] on any ordering mismatch,
  so telegram+matrix multi-transport setups are never silently dropped;
  add regression tests
- named.py create(): keep both reasoning_effort and key params (# noqa:
  PLR0913, matching the codebase convention e.g. tasks/registry.py)
- Update #164 create() test stubs for the new key kwarg
- mypy: guard the optional slack_bolt import behind TYPE_CHECKING/else to
  satisfy strict no-redef
- uv lock: add slack-bolt/slack-sdk (also picks up pre-existing lock drift)
- ruff format integration-touched files

Note: orch._sessions.list_active_for_chat() in slack/bot.py follows the
existing private-access convention (telegram/startup.py does the same);
no public accessor exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PleasePrompto added a commit that referenced this pull request Jul 12, 2026
Reconcile #169 with main's #164 (per-session reasoning effort) and #166
(topic-scoped kills):
- keep #166's kill_by_chat_topic(chat_id, topic_id) instead of #169's kill_all
- keep #164's per-session effort resolution and sync_session_target(reasoning_effort=)
- route the topic branch of switch_model through #169's resolve_session_target

Review fixes for effort persistence, per-provider bucket staleness, and DRY
follow in a subsequent commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PleasePrompto added a commit that referenced this pull request Jul 12, 2026
…olve_session_target

Review fixes on top of the #169 merge:

1. Effort persistence (reconcile with #164): resolve_session_target now takes
   reasoning_effort and writes it onto the created/retargeted session, so a
   topic /model that also picks an effort survives to the next message. The
   topic branch of switch_model passes the resolved effort through.

2. Per-provider staleness: when a session is stale only because the TARGET
   provider bucket hit max_session_messages, reset ONLY that bucket and keep
   the other providers' history instead of wiping the whole shell. Session-wide
   staleness (idle timeout / daily reset) still replaces the full shell.

3. DRY: factor the freshness primitives (_within_message_limit, _time_fresh)
   shared by resolve_session and resolve_session_target; drop the provider-
   mutating _is_fresh_for_target hack.

4. switch_model: extract _persist_switch_target/_SwitchContext to stay within
   complexity limits (no lint suppressions); keep #166's topic-scoped kills.

Tests: update the bucket-wipe assertions to per-bucket reset, add a genuine
idle-stale whole-shell test, add the codex-fresh + claude-count-stale -> /model
claude keeps codex case, and switch the topic mocks to kill_by_chat_topic.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PleasePrompto added a commit that referenced this pull request Jul 12, 2026
…ics)

Integrates ryuhaneul's resolve_session_target with review fixes on top:
per-session reasoning effort is persisted on the switch target (#164
compatibility), message-count staleness now resets only the target
provider bucket instead of wiping all bucket history, and the freshness
logic is shared with resolve_session instead of duplicated.

Closes #169
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants