Skip to content

fix(server): forward steerActiveTurn through wrapSessionProvider - #4113

Open
burakcbdn wants to merge 1 commit into
getpaseo:mainfrom
burakcbdn:fix/forward-steer-active-turn
Open

burakcbdn wants to merge 1 commit into
getpaseo:mainfrom
burakcbdn:fix/forward-steer-active-turn

Conversation

@burakcbdn

@burakcbdn burakcbdn commented Aug 31, 2026

Copy link
Copy Markdown

Linked issue

Closes #4112

Type of change

  • Bug fix
  • New feature
  • Enhancement
  • Refactor
  • Docs

Reasoning

With Default send: Steer, sending a message to a working agent was canceling
its turn and starting a new one. The tool call in flight came back as denied and
the agent restarted from the new message — the exact thing steering exists to
avoid. It looked random because it depends on the provider entry the agent was
created under, not on the agent or the model.

wrapSessionProvider forwards a fixed list of the optional AgentSession
methods and steerActiveTurn was not on it, so a wrapped session did not have
the method at all. steerOrReplaceActiveTurn then took its
{ status: "unavailable" } branch and replaced the turn, which is the correct
behavior for a provider that cannot steer and the wrong one here — the provider
underneath can.

createResolvedProviderClient wraps whenever the provider id differs from the
base client's, or profileModels/additionalModels is non-empty. So this hit
every custom provider, and the built-in claude provider as soon as it had
model overrides in the config. The claude, codex and opencode providers all
implement steerActiveTurn, so all three were affected; I reproduced and
verified the fix on Claude Code.

Goals

  • A wrapped session steers through to the inner session, so steering behaves the
    same whether or not the provider has model overrides configured.
  • The wrap test covers steerActiveTurn, so removing the forward again fails a
    test that actually runs.

Non-goals

  • The exhaustiveness guard sitting next to that test does catch this, but
    tsconfig.server.json excludes src/**/*.test.ts, so no command evaluates it.
    Fixing that means bringing test files into npm run typecheck, which surfaces
    unrelated errors in the same file's FakeSession. Out of scope here; written
    up in bug: steering silently falls back to interrupt + replace for wrapped providers #4112 with the tsgo output.
  • No change to what happens when a provider genuinely cannot steer. That path
    still replaces the turn.

QA

Before, on main (63923fd), with steerActiveTurn added to the wrap test and
the wrapper untouched:

AssertionError: expected [ 'listCommands', 'setModel', …(7) ] to deeply equal [ 'steerActiveTurn', …(9) ]

- Expected
+ Received

@@ -1,7 +1,6 @@
  [
-   "steerActiveTurn",
    "listCommands",

 Test Files  1 failed (1)
      Tests  1 failed (1)

After, same command:

 Test Files  1 passed (1)
      Tests  1 passed (1)

Related suites, unchanged:

npx vitest run src/server/agent/provider-registry.test.ts \
  src/server/agent/provider-registry-wrap.test.ts \
  src/server/agent/agent-manager.test.ts \
  src/server/agent/agent-manager-stream-coalescing.test.ts

 Test Files  4 passed (4)
      Tests  245 passed (245)

I then ran the jobs this diff routes to in CI, with their prerequisite steps, on
Linux:

  • format: npm run format:check — clean, 4196 files.
  • lint: lockfile-lint, npm audit signatures (347 packages verified), and
    npm run lint — clean.
  • typecheck: npm run build:server, npm run typecheck, and the three
    npm pack --dry-run checks — all pass.
  • server-tests: after git fetch origin main and npm run build:server,
    npm run test:unit is 5234 passed / 51 skipped / 2 failed, and
    npm run test:integration is 12 passed plus 1 passed / 10 skipped.

The two unit failures are not from this change:

  • src/utils/paseo-config-file.test.ts > "rejects stale writes when the current
    revision changed before rename" fails identically on unmodified 63923fd,
    including when run alone. Both writes land in the same mtimeMs
    (1788173252565.0713), so it looks like filesystem timestamp resolution here
    (ext4).
  • src/server/hub/hub-cli-contract.test.ts times out at 30s, but only while all
    359 test files run in parallel. Run the way CI runs it —
    npm run test:hub-cli-contract — it passes.

Not run: server-tests (windows-latest), no Windows machine here. The other
required checks (app, playwright, desktop, sdk, relay, cli) are filtered out for
this path, so I did not run them either. My runs were on Node 24 while CI pins
Node 22, and the globally installed agent CLIs here are newer than the versions
CI installs.

In the app: I applied the same one-line change to the installed 0.5.1 build,
restarted the daemon, and re-ran the case that had been failing all day — two
long-running agents under a claude provider entry with additionalModels
configured, a mid-turn message to each with Default send: Steer. Both were
steered; neither was interrupted. Before the change, both were replaced every
time.

Tested on Linux (Ubuntu 24.04.4), self-hosted daemon, Claude Code v2.1.241. The
codex and opencode providers take the same wrapper path but I did not test them
by hand. Not tested on macOS desktop, iOS or Android.

Checklist

  • One focused change
  • npm run typecheck passes
  • npm run lint passes
  • npm run format passes
  • QA evidence
  • Tests added or updated where it made sense

wrapSessionProvider forwards a fixed list of the optional AgentSession
methods and steerActiveTurn was not on it, so any wrapped session lost the
method entirely. steerOrReplaceActiveTurn then took its unavailable branch
and replaced the running turn instead of steering it.

That hits every custom provider, and a built-in provider as soon as
profileModels or additionalModels is configured. The claude, codex and
opencode providers all implement steerActiveTurn, so with Default send set
to Steer a mid-turn message canceled the turn and the tool call in flight
came back denied.

The wrap test already walks the optional methods, so steerActiveTurn joins
that walk and the exhaustiveness list next to it.
@atomlink-ye

Copy link
Copy Markdown
Contributor

Thank you for isolating the wrapper bug. PR #4154 includes your exact commit 75c309ce6b810843b048086ee6e965801e237934 (cherry-picked as c9fec4a31) with your original authorship, because the Hermes generic-ACP steering path needs this forwarder. I called that out explicitly in the PR body so maintainers can coordinate/supersede without losing credit.

@burakcbdn

Copy link
Copy Markdown
Author

Thanks for the heads-up, and for keeping the authorship on it.

Useful to know the generic-ACP path needs the same forwarder — that's a second
case for it beyond the Claude provider, which is where I hit it.

Happy to close this one if the maintainers would rather take #4154; the change
is the same either way. The repro and the trace I used are in #4112 if that's
worth anything there.

@kywoo26

kywoo26 commented Sep 17, 2026

Copy link
Copy Markdown

Confirmed this works. Running v0.8.0 with a custom extends: "claude" provider, steering was completely broken and every message mid-turn just cancelled whatever was running. Patched locally, works now.

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.

bug: steering silently falls back to interrupt + replace for wrapped providers

3 participants