Skip to content

feat(sessions): accept a per-session Claude model on POST /api/sessions - #514

Merged
Ark0N merged 2 commits into
Ark0N:masterfrom
irisitymichaelgrundberg:feat/claude-session-model
Oct 4, 2026
Merged

Ark0N merged 2 commits into
Ark0N:masterfrom
irisitymichaelgrundberg:feat/claude-session-model

Conversation

@irisitymichaelgrundberg

@irisitymichaelgrundberg irisitymichaelgrundberg commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Part of #513 (the Claude half).

A caller can now start one Claude session on a chosen model without writing anything to disk. POST /api/sessions takes an optional model, and the session launches with claude --model <id>.

Why

The only per-request way to pick Claude's model has been modelOverride, which the route writes into <workingDir>/.claude/settings.local.json. That model outlives the session, and every later claude run in the directory starts on it, including runs outside Codeman. A caller that wants one session on Fable and the next on Sonnet has no way to say so without editing the user's checkout.

What it does

  • model in CreateSessionSchema. It is optional, at most 100 characters, and uses the same character set as the registry's model-claude pattern. A value the schema accepts therefore can't be refused at launch. An empty string means no per-session model, as it does for modelOverride.
  • The route prefers it. When the CLI's model source is claude-settings-file, the route now takes body.model first and falls back to the app-wide default model as before. The session hands it to the existing --model slot in Claude's launch template.
  • modelOverride is unchanged. A caller who wants the model written into the case still sends that field. If a caller sends both, the file gets modelOverride and this session runs on model.
  • Claude only. Every other CLI takes its model in its own config object, such as codexConfig.model, so the route refuses a top-level model for them with INVALID_INPUT. Otherwise it would be dropped without a word. The check keys on the registry's model-source capability, so the branch adds no CLI id check.
  • It survives recovery. SessionState now carries model, the model the session launched with. Mux recovery in server.ts and reboot restore both pass it back, so a recovered session relaunches on the same --model and not on the account default. Sessions launched on the app-wide default model keep that one across a restart too.

CLAUDE.md used to say model choice goes through settings.local.json, "NOT --model". It now describes both routes: modelOverride writes a lasting default into the case, and model sets one session's launch flag. The endpoint reference in skills/codeman/reference/endpoints.md and its plugin mirror list model among the create fields. That paragraph's two line references had gone stale, so they now name the handler and sessionCapacityMessage().

Testing

  • npm test passes, as do typecheck, lint and format.
  • The new test/routes/session-routes-claude-model.test.ts creates sessions through app.inject(), starts each one, and reads the model the session hands the mux. It covers:
    • a model the caller names;
    • that model winning over the app-wide default;
    • the default still applying when the caller names none, or names an empty string;
    • model and modelOverride sent together, where the launch gets model and the case file gets modelOverride;
    • a model with shell characters failing the request with 400;
    • a model sent with codex refused before any session is made.
  • The new test/session-model-recovery.test.ts checks that toState() carries the model and that a session rebuilt from that state launches on it. restoreMuxSessions() can't be reached under vitest, so a source check pins both recovery constructors, the way test/remote-wake.test.ts pins its wiring.
  • test/cli-registry-spawn-golden.test.ts pins how a model renders, including one that opens with a dash: it lands as --model "--dangerously-skip-permissions", and Claude's option parser, Commander, takes the word after --model as its value.
  • Each new test fails without the change it covers.

No changeset, following the repository's convention for contributors.

🤖 Generated with Claude Code

POST /api/sessions takes an optional `model`, and a Claude session
launches with `claude --model <id>`. It wins over the app-wide default
model and writes nothing to disk, unlike `modelOverride`, which stays as
it is and still writes the case's .claude/settings.local.json.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… claude

SessionState now carries the model a session launched with, and both
recovery constructors (mux recovery and reboot restore) pass it back, so a
recovered session relaunches on the same --model rather than the account
default. A top-level `model` sent with any other CLI is refused, since
those take their model in their own config object, and an empty string
means no per-session model, as it does for modelOverride.

CLAUDE.md now describes both routes for a Claude model. The tests pin
which of `model` and `modelOverride` reaches the launch and which the
case file, and that a model opening with a dash renders as --model's value.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@irisitymichaelgrundberg
irisitymichaelgrundberg marked this pull request as ready for review October 2, 2026 04:49
@Ark0N
Ark0N merged commit ccd6df8 into Ark0N:master Oct 4, 2026
2 checks passed
Ark0N pushed a commit that referenced this pull request Oct 4, 2026
…dvisor (#514, #515, #530 landing)

Maintainer merge-time fixes for the three PRs that landed together on the
session create / launch / persistence path.

#514 findings (bot verdict merge-with-fixes):
- minor, fixed: SessionState.model was published and persisted for every
  mode, so a codex/opencode cron session reported the app-wide Claude
  default it never ran on. toState() now emits it only where the new
  cliTakesSessionModel() holds (registry capability model.source ===
  'claude-settings-file', no CLI id branch). POST /api/sessions uses the
  same helper for its non-claude refusal, so refusal and publication cannot
  drift. Recovery then hands back undefined for other modes on its own.
- nit, fixed: the `model` schema admitted a leading dash (and '.', '[').
  The first character must now be a letter or digit; still a subset of the
  registry's model-claude pattern, so nothing accepted is refused at launch.
- nit, fixed (reject, the consistent choice): `model` with
  attachRemoteSession was silently dropped. Now a 400 INVALID_INPUT, as
  #514 does for non-claude CLIs and quick-start does for remote cases.
  advisorModel (#530) gets the same refusal there. effort and envOverrides
  keep their older silent ignore on that branch so no existing caller breaks.

#515 finding (bot verdict merge, one nit):
- nit, fixed: the types/session.ts @fileoverview described CodexConfig as
  (model, resumeSessionId); it now lists reasoningEffort, bypass,
  animations and renderMode too.

Audit of the merged combination (not reviewed before):
- The conflict resolutions in session.ts (toState), types/session.ts,
  reboot-restore-routes.ts, server.ts (restoreMuxSessions), CLAUDE.md and
  skills/codeman/reference/endpoints.md (+ plugin mirror) keep both sides
  correctly; nothing was lost or doubled.
- A claude session with both `model` and `advisorModel` launches with
  `--model <id>` and ONE merged `--settings` JSON (ultracode + advisorModel,
  or advisorModel beside `--effort <level>`), on the tmux template
  (including the resume || new variant and with the statusLine exporter)
  and on the direct-PTY fallback. Both values (and effort) survive
  restoreMuxSessions onto a dead pane, a reboot restore into a fresh pane,
  and restartCli/dead-pane respawn via _buildRespawnPaneOptions.
- quick-start and ralph-loop take no per-session `model` (matching #514's
  scope, POST /api/sessions only) and launch on the app-wide default, which
  toState now persists for claude, so recovery stays consistent.
- No defect found in the combination beyond the findings above. Noted, not
  changed: advisorModel is still published for any mode a caller sends it
  with (launch-inert there; the UI and skill send it for claude only).

Tests: test/session-model-recovery.test.ts pins the pair through both
recovery shapes for effort ultracode/high/none, the recovery constructors'
fields, the tmux-manager builder hop, and the codex/opencode/shell
non-publication; test/advisor-model.test.ts pins the launch lines and a
real direct-PTY Session's pty.spawn argv; the route test covers flag-shaped
models, attach refusals and the published fields. Docs: SessionState.model
docstring, the reboot-restore-registry header, the golden test comment and
the CLAUDE.md model/advisor bullets.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Ark0N pushed a commit that referenced this pull request Oct 4, 2026
…#520, #521, #522, #523, #524, #530, #531)

One consolidated minor changeset with the Thanks block first; the four contributor changesets (#520, #521, #522, #523) are folded into it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Ark0N

Ark0N commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Merged and shipped in 1.34.0. Thanks @irisitymichaelgrundberg! Keying the non-Claude refusal on the registry's model source instead of a CLI id, and carrying the model through both recovery paths, is exactly right. The CLAUDE.md rewrite of the "NOT --model" rule was careful too.

A few items were applied while landing (06c4c7d):

  • SessionState.model is now published and persisted only for a CLI that takes a session model (a new cliTakesSessionModel(), same capability check as your refusal), so a codex or opencode cron session no longer reports Claude's default.
  • The model schema refuses a leading dash, since the value lands in argv.
  • model together with attachRemoteSession is a 400 now, instead of being silently ignored. advisorModel (new in this release) gets the same refusal.
  • This landed alongside the new per-session advisor. A session with both gets --model <id> plus the one merged --settings JSON on both launch paths, and both survive every recovery path. Tests pin the combination.

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