Skip to content

Modes: a switch mid-task takes effect on the model's next turn - #395

Merged
rromenskyi merged 4 commits into
mainfrom
mode-next-turn
Oct 20, 2026
Merged

rromenskyi merged 4 commits into
mainfrom
mode-next-turn

Conversation

@rromenskyi

Copy link
Copy Markdown
Contributor

shift+tab during a task printed "applies on the next task", and that was accurate. The pending mode was applied only when the whole task ended (applyPendingMode in taskDoneMsg), while the README claimed "the next turn". It now applies on the next turn.

How it works:

  • The UI queues the choice (app.queueMode). The agent takes it at the start of the model's next turn (SetModeSwitch → app.takeMode, through Agent.switchMode). That happens before the call and never in the middle of a turn, so all of a turn's tool calls are judged under one mode.
  • Manual takes effect for the call gate right away, because app.manual is atomic. Plan mode is set by the agent itself. A switch into plan sends plan mode's directive into the conversation, and a switch out of plan sends its OFF note. The plan backstop in execOne follows the new value.
  • A mode event updates the UI. It logs ⇄ manual mode on — from step N, updates the indicator, and clears the pending choice. A switch into plan mid-task gets the PLAN READY handshake at the end.
  • If the task ends before the model takes another turn, the choice applies to the next task, as before, and the queue is dropped.
  • Read-only mode and detached runs are not switched.
  • Agent.planMode is now an atomic.Bool, since the agent writes it between turns.

Tests:

  • TestAModeChosenMidTaskTakesEffectOnTheNextTurn (agent) covers auto→plan mid-run (second write refused, directive sent, event emitted), plan→auto (OFF note sent, write runs) and read-only staying put.
  • TestAModeChosenMidTaskIsTakenAtTheNextTurn (cmd) covers the queue, a single take, manual applying for the gate, the event updating the UI, the plan handshake, and a task ending before the take.

A sabotage pass made 11 mutations, and every one failed a test. The wiring line a.ag.SetModeSwitch(a.takeMode) in wire() has no direct test.

🤖 Generated with Claude Code

shift+tab during a task said "applies on the next task", and did: the
pending mode was applied only when the whole task ended — while the
README claimed "the next turn". Now it is the next turn.

The choice is queued for the agent, which takes it at the start of the
model's next turn, before the call — never in the middle of a turn, so a
turn's calls are judged under one mode. Manual takes effect for the call
gate right there; a switch into or out of plan mode goes into the
conversation as plan mode's directive or its OFF note, and the plan
backstop follows. A "mode" event brings the UI along and logs
"⇄ manual mode on — from step N"; switching into plan mid-task gets the
PLAN READY handshake at the end. A task that ends before the model takes
another turn applies the choice to the next task, as before. Read-only
mode is not switchable this way.

Agent.planMode is atomic now: the agent writes it between turns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Code review skipped — your organization has no extra usage available to pay for this review.

If your organization's extra usage balance is empty, an organization admin can add extra usage credits at claude.ai/admin-settings/usage. If its monthly spend limit was reached, an admin can raise it on the same page. If neither applies, contact Anthropic support.

Once extra usage is available, someone with write access to this repository can comment @claude review on this pull request to trigger a review.

rromenskyi and others added 3 commits October 19, 2026 21:28
- takeMode sets the app's plan mode too, on the agent's goroutine: a
  sub-agent spawned on the turn after a switch to plan read the old mode
  until the UI caught up, and could write. app.planMode is atomic now.
- The "mode" event only clears the pending choice. It called setMode,
  so an event read late could set back a newer switch the agent had
  taken since, mid-turn.
- PLAN READY is decided by the mode the run ended in, not by the order
  its last mode event and taskDoneMsg arrive in.
- force-detach applies a pending choice and drops the queue, and an idle
  shift+tab drops it too: the fresh agent took the abandoned run's mode
  on its first turn, over one chosen since.
- shift+tab works while an approval waits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- What the task's end (or a force-detach) applies is what is still in
  the queue — what the agent did not take — instead of the UI's pending
  copy, which a late or stale "mode" event had to clear by name. The
  event now only logs.
- setMode empties the queue: /plan typed after a shift+tab during
  /compact was undone by the next task's first turn.
- takeMode takes and stores under one lock, and force-detach pops the
  queue before building the fresh agent, so a detached run's late take
  cannot leave the app and the new agent in different modes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A run paused between its own detached check and the take could, after a
force-detach and a new task, take the choice queued for the new run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rromenskyi
rromenskyi merged commit 4521df1 into main Oct 20, 2026
2 checks passed
@rromenskyi
rromenskyi deleted the mode-next-turn branch October 20, 2026 03:57
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.

1 participant