Skip to content

Steer Copilot, OpenCode and Cursor turns by stopping them and sending the message next - #1015

Merged
sak0a merged 2 commits into
mainfrom
port/steer-by-restart
Oct 31, 2026
Merged

sak0a merged 2 commits into
mainfrom
port/steer-by-restart

Conversation

@sak0a

@sak0a sak0a commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

What Changed

  • GitHub Copilot, OpenCode and Cursor can now be steered. They can't add a message to a running turn, so a steer stops the turn and the message is sent next. Until now the composer said "steering unavailable" for them, and the message waited for the turn to end.
  • Those adapters declare turnSteering: "interrupt-restart". For them, ProviderService.steerTurn interrupts the expected turn and then defers the steer (ProviderTurnNotSteerableError, reason restarting). The message goes back into the node's follow-up queue and is sent as the next turn once the stopped turn settles.
    • The deferral carries restartedTurn (an optional flag), and the node's queue moves that message to its head, so the stopped turn is followed by the steered message, not an earlier follow-up.
    • A node's own stop holds no queue; only a person's Stop does.
    • If the turn cannot be stopped, the message stays queued and the activity says so.
  • Their provider status carries turnSteeringMode: "interrupt-restart". It is a plain string, so a mode an older client does not know reads as no steering instead of failing the provider decode. A shared providerSteering() in client-runtime reads it for web and mobile.
  • Wording: the composer says "Stop this turn and send this next · ⌘↵ to queue instead", and a queued message's Steer button (web and phone) says "Stop the running turn and send this message next".
  • While approvals wait, a message still redirects on these providers: it declines the approvals and then stops the turn (documented).
  • Only explicit steers stop a turn: with the follow-up setting on Queue, Enter still waits for the turn to end. ACP registry agents keep queueing, as in T3.
  • Docs: docs/providers/approval-response-policy.md → "Steering providers without native steering".

Why

Port batch 9c, item "capability-driven adapter degradation". The report's recommendation was to give every provider steer semantics through an interrupt-and-restart steering mode, rather than rewrite capabilities into T3's 11 groups. This follows T3's policy (docs/orchestration-v2/provider-capability-system.md: "active steering unsupported → interrupt active turn and restart the run"; CommandPolicy.ts decideSteeringExecution; adapters with supportsSteeringByInterruptRestart: true for Cursor, OpenCode and ACP, false for the ACP registry), adapted to Ryco's server-owned follow-up queue.

T3's ForwardCompatibleArray/Optional decoding was not ported wholesale. The new status field uses a plain string, the same forward-compatible pattern Ryco already uses for lineage relationships.

Validation

  • bun run check and bun run check:changed pass. Server boot +931 B.
  • New tests, all failing without the change:
    • ProviderService.test.ts: a Cursor-shaped adapter in interrupt-restart mode has the expected turn interrupted, gets a deferred steer with the restart wording and never has steerTurn called. A failed interrupt defers with "could not be stopped. The message stays queued."
    • logic.test.ts (client-runtime): providerSteering reads native, restart, an unknown future mode (no steering) and absent status.
    • composerFollowUp.test.ts: the restart wording for both follow-up settings.
    • decider.followUpQueue.test.ts: a steer deferred with restartedTurn moves to the head of a queue with earlier follow-ups; turnSteerFailure.test.ts: only the restarting reason sets the flag.
  • Not verified against real Copilot, OpenCode or Cursor processes that a stopped turn always settles promptly. If one does not, the message waits until the turn settles, as before.

🤖 Generated with Claude Code

@github-actions github-actions Bot added size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. labels Oct 31, 2026
@sak0a
sak0a force-pushed the port/steer-by-restart branch 28 times, most recently from ec1087f to e52e250 Compare October 31, 2026 09:48
@sak0a
sak0a force-pushed the port/steer-by-restart branch 26 times, most recently from 9de8d7d to df40d83 Compare October 31, 2026 14:57
sak0a and others added 2 commits October 31, 2026 16:03
… the message next

Steering a running turn worked only for Claude and Codex; on GitHub Copilot, OpenCode and Cursor
the composer said "steering unavailable" and the message waited for the turn to end. Those
providers can't add a message to a running turn, but they can stop it. They now report
turnSteeringMode "interrupt-restart" (adapter capability turnSteering "interrupt-restart"), and a
steer stops the turn and defers: the message stays in the node's follow-up queue and is sent as
the next turn. A node's own stop holds no queue, only a person's Stop does. If the turn cannot be
stopped, the message stays queued and the activity says so.

The composer and a queued message's Steer button say "Stop this turn and send this next". The
status field is a plain string, so a mode an older client does not know reads as no steering
instead of failing the provider decode. ACP registry agents keep queueing, as in T3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e phone too

Review findings on restart steering:
- A deferred steer stayed in its queue position, so with other follow-ups queued the stopped
  turn was followed by an earlier message, not the steered one. The deferral now carries
  restartedTurn (an optional flag, like deliveryUncertain), and the node's queue moves that
  message to its head.
- The phone's queued-message Steer button and web's aria-label kept "into the active turn"
  wording; both now say the turn is stopped and the message sent next.
- Docs: a message sent while approvals wait still redirects on these providers, which declines
  the approvals and then stops the turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sak0a
sak0a force-pushed the port/steer-by-restart branch from df40d83 to 70f2e24 Compare October 31, 2026 15:03
@sak0a
sak0a merged commit 6259b68 into main Oct 31, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant