Skip to content

agentHost: resolve user shell environment for agent host process#312199

Merged
roblourens merged 2 commits intomainfrom
roblou/agents/agent-host-process-shell-env-lookup
Apr 23, 2026
Merged

agentHost: resolve user shell environment for agent host process#312199
roblourens merged 2 commits intomainfrom
roblou/agents/agent-host-process-shell-env-lookup

Conversation

@roblourens
Copy link
Copy Markdown
Member

Spawn the agent host with the user's resolved shell environment merged in (PATH and friends from the login shell), matching what other VS Code processes already do via getResolvedShellEnv. Without this, tools and terminals launched by the agent host on macOS/Linux GUI launches don't see the user's PATH.

What changed

  • Both ElectronAgentHostStarter and NodeAgentHostStarter now call getResolvedShellEnv and merge the result into the spawned agent host's env. The shell env resolution is cached at module scope in shellEnv.ts, so reusing the same call the main process already made is essentially free.
  • IAgentHostStarter.start() is now async (Promise<IAgentHostConnection>). AgentHostProcessManager._start() and ServerAgentHostManager._start() await it and guard against the manager being disposed mid-await.
  • The Electron starter now also injects IConfigurationService.

Race fix in _onWindowConnection

Once start() became async (with an await on shell-env resolution), the renderer's vscode:createAgentHostMessageChannel IPC could race ahead and call utilityProcess.connect() before utilityProcess.start() had actually run. connect() calls postMessage(port) which silently returns false when the underlying process isn't started — the renderer's MessagePort got dropped, and the renderer never saw any agents advertised.

_onWindowConnection now awaits a DeferredPromise<void> (utilityProcessStarted) that completes once utilityProcess.start() has run.

Tests

  • Mock IAgentHostStarter in serverAgentHostManager.test.ts updated for the async signature; all tests now await a waitForStart() microtask before firing channel events.

Why pty host doesn't need this

Pty host doesn't pre-resolve shell env into the spawned child — it just inherits process.env. Resolved shell env is used later, on demand, only for terminal profile detection and per-terminal launch configs (which are sent to the pty host via IPC). The agent host is different: tools it spawns internally need PATH baked into its own process env, because they aren't routed through a workbench-built launch config.

(Written by Copilot)

Spawn the agent host with the user's resolved shell environment merged
in (PATH and friends from the login shell), matching what other VS Code
processes do via getResolvedShellEnv. Without this, tools and terminals
launched by the agent host on macOS/Linux GUI launches don't see the
user's PATH.

Both ElectronAgentHostStarter and NodeAgentHostStarter now resolve the
shell env before spawning. IAgentHostStarter.start() is now async; the
process managers await it and guard against being disposed mid-await.

In the Electron starter, the renderer's createMessageChannel request
could race ahead of the now-async start() and call utilityProcess.connect()
before utilityProcess.start() had run, silently dropping the MessagePort
and leaving the renderer with no agents. _onWindowConnection now awaits
a DeferredPromise that completes once the utility process has actually
been spawned.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 23, 2026 19:00
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR updates the agent host startup flow to incorporate the user’s resolved login-shell environment (e.g., PATH on macOS/Linux GUI launches) and adjusts the agent host starter/manager APIs to support async startup safely.

Changes:

  • Make IAgentHostStarter.start() async and update the agent host process managers to await startup and dispose connections if the manager was disposed mid-start.
  • Resolve and merge getResolvedShellEnv(...) into the spawned agent host process environment (Electron utility process and Node child-process starter).
  • Update server agent host manager tests to accommodate async startup timing.
Show a summary per file
File Description
src/vs/server/test/node/serverAgentHostManager.test.ts Updates tests for async starter signature and event wiring timing.
src/vs/server/node/serverAgentHostManager.ts Awaits async starter and adds disposed-guard after startup.
src/vs/platform/agentHost/node/nodeAgentHostStarter.ts Resolves shell env and passes it to the forked agent host process; starter becomes async.
src/vs/platform/agentHost/node/agentHostService.ts Awaits async starter and adds disposed-guard; adjusts _started timing.
src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts Resolves shell env for the utility process, makes startup async, and adds a deferred gate for connect timing.
src/vs/platform/agentHost/common/agent.ts Changes IAgentHostStarter.start() to return a promise.
src/vs/code/electron-main/app.ts Passes IConfigurationService into ElectronAgentHostStarter.

Copilot's findings

Comments suppressed due to low confidence (2)

src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts:123

  • Because _onWindowConnection is now async, any exception/rejection inside it will turn into a rejected Promise. Since the IPC registration uses a plain validatedIpcMain.on(...) listener (which does not await Promises), rejections can become unhandled promise rejections. Consider ensuring _onWindowConnection never rejects (wrap body in try/catch + log) or have the IPC listener explicitly handle the Promise result.
	private async _onWindowConnection(e: IpcMainEvent, nonce: string): Promise<void> {
		this._onRequestConnection.fire();

		// Wait for utilityProcess.start() to actually run before calling connect(),
		// otherwise the MessagePort posted via connect() is silently dropped.
		await this.utilityProcessStarted?.p;

src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts:69

  • utilityProcessStarted is awaited in _onWindowConnection, but it’s only completed after utilityProcess.start(...). If UtilityProcess.start throws (e.g. fork failure), the deferred promise will never settle and future window connections can hang awaiting it. Consider settling the deferred in a try/finally (and calling error(...) on failure) so _onWindowConnection can proceed to the “process not running” path instead of waiting indefinitely.

This issue also appears on line 117 of the same file.

		this.utilityProcess.start({
			type: 'agentHost',
			name: 'agent-host',
			entryPoint: 'vs/platform/agentHost/node/agentHostMain',
  • Files reviewed: 7/7 changed files
  • Comments generated: 3

Comment thread src/vs/platform/agentHost/electron-main/electronAgentHostStarter.ts Outdated
Comment thread src/vs/platform/agentHost/node/agentHostService.ts Outdated
Comment thread src/vs/server/node/serverAgentHostManager.ts Outdated
- ElectronAgentHostStarter: spread shellEnv after process.env so the
  resolved login shell PATH actually wins over the GUI-launched env.
  (NodeAgentHostStarter is fine as-is: ipc.cp.Client merges process.env
  before the options env, so shellEnv there already wins.)
- AgentHostProcessManager._start / ServerAgentHostManager._start: wrap
  the body in try/catch so a rejection from starter.start() doesn't
  surface as an unhandled promise rejection. Reset state on failure so
  future starts can retry. Server manager applies the same MaxRestarts
  policy as the unexpected-exit path.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@roblourens roblourens marked this pull request as ready for review April 23, 2026 22:06
@roblourens roblourens enabled auto-merge (squash) April 23, 2026 22:06
@vs-code-engineering
Copy link
Copy Markdown
Contributor

📬 CODENOTIFY

The following users are being notified based on files changed in this PR:

@deepak1556

Matched files:

  • src/vs/code/electron-main/app.ts

@roblourens roblourens merged commit f158e93 into main Apr 23, 2026
26 checks passed
@roblourens roblourens deleted the roblou/agents/agent-host-process-shell-env-lookup branch April 23, 2026 22:35
@vs-code-engineering vs-code-engineering Bot added this to the 1.118.0 milestone Apr 23, 2026
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.

3 participants