Skip to content

fix(server): resolve the configured shell for session terminals - #61353

Open
WuJunkai2004 wants to merge 4 commits into
anomalyco:devfrom
WuJunkai2004:fix/session-terminal-configured-shell
Open

WuJunkai2004 wants to merge 4 commits into
anomalyco:devfrom
WuJunkai2004:fix/session-terminal-configured-shell

Conversation

@WuJunkai2004

@WuJunkai2004 WuJunkai2004 commented Nov 3, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #51051

Type of change

  • Bug fix

What does this PR do?

Session terminals resolve their shell through ShellSelect.environment(), which only reads process.env.SHELL. That variable is normally unset on Windows, so the configured shell is ignored and terminals open PowerShell. The agent's shell tool resolves through the Location-scoped service and honors the configured shell; only terminals diverged (regressed in 2.2.4 when terminals moved to persistent PTYs in #60346).

ShellSelect is Location-scoped: it is only provided to handlers by the location middleware, and the experimental persistent-pty endpoints carry no middleware (unlike config.*, which the first revision of this patch modelled itself on — that leaked ShellSelect.Service into the routes layer and 500'd every create; thanks to the reviewer for catching it).

So this resolves the shell inside the terminal's session location, the same way middleware/session-location.ts does: sessionInfo() → instances.provide(session) → ShellSelect.Service, awaiting plugin activation first so the config→shell state is populated. Resolution happens lazily only when payload.command is absent (explicit commands are untouched) and is best effort: a terminal whose session or location cannot be resolved keeps the environment shell, matching the old behavior (some existing tests create terminals on session IDs that were never registered).

How did you verify your code works?

Added a regression test in packages/server/test/persistent-pty.test.ts: it configures a shell via a project opencode.json, creates a session at that location, creates a terminal without a command, and asserts terminal.command is the configured path and that the terminal actually ran it. All seven pre-existing tests pass an explicit command, so they never enter the new resolution path.

Verified on a Linux box (Debian, bun 1.4.4, frozen lockfile): the full persistent-pty.test.ts suite passes 8/8 (7 pre-existing + the new one, which is skipped on Windows hosts), and turbo run typecheck:tsc --filter=@opencode/server reports the same error set as the unmodified base.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #51994, #57310

Session terminals resolved their shell via ShellSelect.environment(),
which only reads process.env.SHELL, so the shell config option was
ignored (always PowerShell on Windows). Resolve through the
Location-scoped ShellSelect.Service in the persistentPty.create
handler instead, matching the agent shell tool and pty.ts.

Fixes anomalyco#51051

@opencodejr opencodejr 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.

This doesn't work yet: with this change, every request to create a session terminal fails with a 500.

In packages/server, bun test test/persistent-pty.test.ts passes 7/7 on the base and fails 8/8 with this PR, including the new test. tsc -b (the typecheck:tsc script) goes from 0 errors on the base to 89 errors across the server tests. Each one says ShellSelect.Service is required but never provided. The typecheck script itself (bun check) fails on the base too, because the script isn't found.

The cause is that persistentPty.create isn't behind any location middleware. Unlike the config.* routes this was modelled on, nothing provides the location-scoped ShellSelect.Service there (see inline). I couldn't drive the TUI repro on either checkout: the simulated model's reply never arrived.

Public API: unchanged.

Related: #51994, #57310

.handle(
"persistentPty.create",
Effect.fn(function* (ctx) {
const shell = yield* ShellSelect.Service

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

persistentPty.create has no location middleware (see protocol/src/groups/persistent-pty.ts), so the location-scoped ShellSelect.Service isn't provided here. This yield* fails every create with a 500, even when a command is sent, and the R requirement leaks into the routes layer, which is where the 89 tsc errors come from. The shell needs to be resolved inside the session's location. Two ways: attach SessionLocationMiddleware to this endpoint, or provide the session's instance (as middleware/session-location.ts does with instances.provide(session)) before yielding ShellSelect.Service. Resolving it lazily only when ctx.payload.command is undefined would also keep explicit-command terminals independent of it.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Addressed in ba936f3 — the handler now provides the session's instance itself (same as middleware/session-location.ts: sessionInfo → instances.provide → ShellSelect.Service) and awaits plugin activation before resolving, since the config→shell state is populated asynchronously. Kept it lazy, so explicit commands never resolve, and best-effort: terminals whose session can't be resolved keep the environment shell, which a couple of the existing tests depend on. The full persistent-pty suite passes 8/8 on Linux with the new test included, and server typecheck:tsc matches the base error set.

Review on the first revision showed ShellSelect.Service is not
provided to the persistentPty handlers: the experimental endpoints
carry no location middleware, so the yield failed every terminal
create and leaked into the routes layer.

Resolve in the session's location instead, the same way
middleware/session-location.ts does: sessionInfo, then
instances.provide, then ShellSelect.Service, awaiting plugin
activation so the config-to-shell state is populated. Resolution is
lazy (explicit commands never enter it) and best effort: terminals
whose session cannot be resolved keep the environment shell, which
the existing tests depend on.

Fixes anomalyco#51051
@WuJunkai2004

Copy link
Copy Markdown
Author

Good catch — you were right on all three points. Reworked in ba936f3: the handler now resolves the shell in the session's location itself (sessionInfo → instances.provide → ShellSelect.Service, mirroring middleware/session-location.ts), and only when the payload has no command. Resolution is best effort — terminals whose session can't be resolved keep the environment shell, which a few existing tests depend on (they create terminals on session IDs that were never registered).

One thing that took a while to find on top of your notes: the config→shell state is populated by plugin activation, which is asynchronous, so the resolve has to await plugin activation first — without that it silently returned the environment shell in my Linux runs.

The full persistent-pty suite now passes 8/8 on Linux (7 pre-existing + the new regression test, which is skipped on Windows hosts), and server typecheck:tsc reports the same error set as the unmodified base. Protocol is back to untouched — no API surface change anymore.

@opencodejr opencodejr 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.

The 500 on terminal creation is fixed: persistent-pty.test.ts passes 8/8, and the new test fails on the base, where the terminal gets /bin/sh.

One thing still needs fixing: tsc -b in packages/server goes from 0 errors on the base to 89 with this PR. The services that leak into the routes layer are now Session.Service and Instance.Service, because they're yielded inside the per-request function (see inline). Locally, yielding them once in the group's setup brings it to 0 errors with all tests still passing.

Public API: unchanged.

// middleware/session-location.ts provides them. Resolution is best effort: terminals whose session or
// location cannot be resolved keep the process-global environment shell, as before this change.
const configuredShell = Effect.fn("PersistentPty.configuredShell")(function* (sessionID: Session.ID) {
const sessions = yield* Session.Service

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Yielding Session.Service and Instance.Service inside the per-request function adds them as HttpRouter.Request<"Requires", …> requirements of the routes layer. That's where the 89 tsc -b errors in the server tests come from now (the base has 0). Yield them once in the group's outer Effect.gen, next to PtyTicket.Service, and close over them here. I tried that locally: it gives 0 type errors and 8/8 in persistent-pty.test.ts.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in cbaefe4 — Session.Service and Instance.Service are now yielded once in the group's setup next to PtyTicket.Service, and configuredShell closes over them. typecheck:tsc is clean end to end now, and the persistent-pty suite stays at 8/8 on Linux. (For the record, the 89 errors were never Windows-specific noise on my side either — an earlier base comparison I ran was contaminated by an already-committed leak, which is why I misread it as environmental. Your 0-on-base reading was the correct one.)

Yielding Session.Service and Instance.Service inside the per-request
resolution added them as requirements of the routes layer (89 tsc -b
errors in the server tests on an otherwise clean base). Yield them
once in the group's setup next to PtyTicket.Service and close over
them instead. Server typecheck:tsc is clean end to end now, and the
persistent-pty suite stays at 8/8 on Linux.

@opencodejr opencodejr 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.

Both earlier points are addressed. Session terminal creation no longer returns a 500, and tsc -b in packages/server is back to 0 errors, the same as the base. persistent-pty.test.ts passes 8/8, and the new test fails on the base (the terminal gets /bin/sh instead of the configured shell). I didn't find any other issues.

Public API: unchanged.

@opencodejr opencodejr 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.

The new commit only merges dev; the change itself is the same as before. On top of the new dev, persistent-pty.test.ts passes 8/8 and tsc -b in packages/server reports 0 errors. Both earlier points stay addressed, and I found nothing new.

Public API: unchanged.

This branch has not been deployed

No deployments
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.

session terminal ignores the "shell" config option (always PowerShell on Windows)

1 participant