Skip to content

Four controls decide which CLI a coding session launches, and the one the console calls "Coding CLI" is read by nothing — switching to Codex kept burning the Claude session limit #549

Description

@serge-ivo

The owner's report

"hit the session limit again even after the user thought they had switched to Codex… there are two separate settings… changing only one was not enough."

There are four, not two, and the one the console labels "Coding CLI" — the one on the Settings tab, under the heading "Agent settings" — is read by no code at all.

Verified: the setting has zero readers

It is live on every Repo Coder. GET /v1/agents/agent_coder_repo/settings-schema, production, 2026-08-13:

{ "id": "engine", "label": "Coding CLI", "type": "select",
  "description": "Which CLI this agent drives on your machine.",
  "options": [ {"value":"claude","label":"Claude Code"},
               {"value":"codex","label":"Codex"},
               {"value":"grok","label":"Grok"} ],
  "default": "claude" }

It is rendered by store/console/src/tabs/SettingsTab.tsx:442-445 ("Agent settings"), stored on agent_instances.config.settings.engine, and:

$ grep -rn "settings.engine\|\[\"engine\"\]" --include="*.ts" workers/api/src | grep -v '\.test\.'
(no matches)
$ grep -rn "'engine'\|\"engine\"" --include="*.ts" --include="*.tsx" store/console/src \
    | grep -vi "engineId\|engineLabel\|engineRuntime\|codingEngine\|engineAuth\|engines"
store/console/src/pages/Usage.tsx:212   ← the ai_usage `kind`, unrelated

Nothing reads it. Its only effect is that agent-think.ts injects it into the chat prompt's ## Settings block, so the agent is told its engine is Codex while the runner spawns something else.

The four controls that DO decide, in the order they win

workers/api/src/routes/coding.ts:136-146:

	// One active session per repo — a second would share the repo's single working
	// directory and conflict … Reuse the live one.
	const existing = await getActiveSessionForRepo(c.env, instanceId, uid, repoId);
	if (existing) {
		const runnerConnected = (await startSessionOnRunner(c.env, instanceId, uid, existing, repo)).conn != null;
		return c.json({ session: existing, runnerConnected, reused: true }, 200);
	}
	const { command, clientType } = await resolveEngine(c.env, instanceId, uid, body.engineId ?? body.clientType ?? repo.defaultClient);

and lib/coding-engines.ts:318-320:

const eng = engines.find((e) => e.id === engineId) ?? engines.find((e) => e.id === defaultEngineId) ?? engines[0];
# Where Set from Beats
1 coding_sessions.launch_command fixed when the session was created everything — the reuse branch returns before resolveEngine is ever called
2 coding_repos.default_client createRepo, defaulting to "claude" (coding-store.ts:192) 3 and 4
3 agent_instances.config.defaultEngineId console ⚙ CLI engines panel 4
4 agent_instances.config.settings.engine console Settings → Agent settings → "Coding CLI" nothing

So the visible, obvious, agent-named control is last and inert, and the control that actually decides is #1 — a value frozen on a session that the console's "start a session" button silently reuses. A user who changes any setting and clicks start gets {"reused": true} and the old engine, with no message saying so.

Current state, measured — the controls agree today, so this is latent

GET …/settings and GET …/coding/engines for all nine of this owner's Repo Coder instances, 2026-08-13:

AIPA coder    setting.engine=codex   defaultEngineId=codex   codex exec --sandbox danger-full-access
Heartfull     setting.engine=claude  defaultEngineId=claude   claude --dangerously-skip-permissions
Chess coder 2 setting.engine=claude  defaultEngineId=claude   claude --dangerously-skip-permissions
FIS coder / Chess coder / PAS Coder / FWS platform / FGS platform / FAS platform — all claude/claude

No divergence right now (he evidently fixed both by hand after the incident). The mechanism that produced the incident is untouched, and nothing keeps the four in sync or reports a disagreement.

This is the same defect the platform already fixed once

Migration 0102_coder_repo_drop_repo_setting.sql deleted the Repo Coder's repo setting for exactly this reason, and its header reads as if it were written about engine:

"Which repo does this agent work on" was written down in two places. … The reported bug was "I updated it, but it is still using the old one", and both halves were true. … Half a wire is worse than none: it makes the field look connected.

The repo setting had half a wire (attachSettingRepo, create-only). engine has none. It survived that cleanup, and 0092_coder_repo_engine_copy.sql — which touched this exact field — only corrected its description ("in tmux" → "on your machine") while leaving it inert.

What to do, cheapest first

  1. Delete the engine field from the Repo Coder's settingsSchema, by migration, the way 0102 did for repo. One UPDATE agents. Stored values orphan harmlessly (they already do nothing) and the console stops rendering it. This alone removes the control that caused the reported loss.
  2. Tell the user when a session was reused. coding.ts:139-142 already returns reused: true and the console ignores it. If the reused session's launch_command does not match what resolveEngine would have picked, say so: "Reusing the live Claude Code session on this repo. End it to start a Codex one." This is the half that actually bit him — even with one control, a live session outlives the change.
  3. Then decide where "which CLI" lives — repo, or instance. Today coding_repos.default_client silently outranks config.defaultEngineId, so the ⚙ CLI engines default is itself only sometimes honoured. Making the repo settings sheet ([bug] A repo's folder cannot be changed — the console shows it as read-only text and the update API has no parameter for it, so a wrong path can only be deleted #410) the single home matches 0102's conclusion that a repo's facts live on the repo row; the alternative is dropping repo.defaultClient from the ?? chain. I would put it on the repo, because the engine is a property of the codebase (Codex for one repo, Claude for another) far more often than of the agent. Owner's call; either way it must be one place.

Alternatives considered and rejected

  • Wire settings.engine through to resolveEngine. Rejected — that is the mirror image of 0102's reasoning. It makes a typed settings field mutate coding_repos/coding_sessions state, adds a fifth writer, and creates the sync that 0102 says is "a disagreement waiting to happen".
  • Make the settings field authoritative and delete the ⚙ panel. Rejected: the panel edits {id,label,command} presets — the actual command line, flags included (codex exec --sandbox danger-full-access). A three-option select cannot express that, and coding-engines.ts:232-247 documents why the write-flag per engine matters.
  • Auto-restart the session when the engine choice changes. Rejected: ending a live coding session out from under a running turn to honour a settings save is a destructive side effect of a config write. Tell the user; let them choose.

Acceptance criteria

  • GET /v1/agents/agent_coder_repo/settings-schema no longer contains an engine field.
  • Starting a session on a repo with a live session returns a message the console shows, naming the engine that is actually running and how to change it.
  • A test asserts that no field id in any seeded settingsSchema is also a key that resolveEngine/resolveEngineEnv consults — the general form of this bug, and the one seed-drift.test.ts is already positioned to carry.

Regression risk

Removing a settingsSchema field is proven safe by 0102, with the same orphaning behaviour. The real risk is in step 3: repo.defaultClient defaults to "claude" on every repo created (coding-store.ts:192), so dropping it from the ?? chain would silently move every existing repo onto the instance default. If that path is taken, migrate default_client into defaultEngineId first, or scope the change to repos where the two disagree.

Verified from source and from production reads of the live schema, settings and engine presets. Not verified: that the historical Codex/Claude divergence was #4-vs-#3 rather than #1-vs-#3 — the settings have since been aligned by hand and neither carries an audit trail. Both paths are open and both are fixed by the steps above.

Related: #411 / #520 / migration 0102 (the same defect for repo), #92 (edited this field's description without noticing it was inert), #248 (which credential a session actually used).

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend / Worker / API workbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions