Skip to content

Agent Host: Manage idle Dev Container lifecycle - #333955

Draft
Christof Marti (chrmarti) wants to merge 1 commit into
mainfrom
chrmarti/devcontainer-session-lifecycle
Draft

Agent Host: Manage idle Dev Container lifecycle#333955
Christof Marti (chrmarti) wants to merge 1 commit into
mainfrom
chrmarti/devcontainer-session-lifecycle

Conversation

@chrmarti

Copy link
Copy Markdown
Collaborator

Summary

Stop session-owned Dev Containers after all of their sessions become idle, restart them on demand for later work, and remove them before archiving their mounted worktrees.

Tracking issue: #317380

Session Context

Key decisions from the development session:

  • Stop at host idleness: A container is stopped only after every session on its provider has left active or input-needed state, and no unsent draft remains.
  • Retain the provider: Stopping withdraws the runtime connection but preserves the dynamic provider and cached sessions so a later send can restart the container and reconnect transparently.
  • Suppress automatic resurrection: The shared process records a stopped workspace and rejects automatic relay reconnects. Only an explicit user operation can resume it, and suspension clears only after a successful connection.
  • Remove before worktree cleanup: Archive waits for the host to accept the archived state, removes the container, and only then archives/removes the mounted worktree.
  • Preserve shared consumers: Archiving one session does not remove a container while another unarchived session or unsent draft still uses that provider.
  • Keep local paths authoritative: The existing opaque worktree handle remains the remote metadata boundary; container operations use the locally retained source/worktree URI.

Changes

  • Add shared-process operations for stopping and force-removing the known container for a workspace.
  • Serialize container connect, stop, and remove operations and disconnect all relays before Docker lifecycle commands.
  • Preserve stopped state across failed resumes and block background reconnects from other windows.
  • Keep the runtime provider registered while stopped and reconnect it before subsequent sends or archive operations.
  • Stop after all sessions become idle, while preserving active sessions and unsent drafts.
  • Confirm archive actions through a retained session subscription before container/worktree teardown.
  • Recreate the worktree before starting the replacement container on unarchive.
  • Add cross-process coverage for Docker operations, reconnect suppression, failures, drafts, shared sessions, and rejected archives.

Validation

  • 337 affected unit tests passing, 11 pending
  • npm run typecheck-client
  • npm run transpile-client
  • ESLint on changed TypeScript files
  • Repository pre-commit hygiene
  • git diff --check

A full live Dev Container run from main remains blocked by the independent AHP version mismatch between main (0.9) and the currently published Insiders CLI (provisional 1.0).

Stop Dev Containers when their sessions become idle, reconnect them on demand, and remove containers before archiving mounted worktrees. Coordinate suspension and resume across renderer connections and cover failure and shared-session cases.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI balanced review requested due to automatic review settings September 2, 2026 09:05

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Idle detection, multi-window coordination, reference races, and worktree ownership contain unresolved lifecycle defects.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review tier: Balanced
Findings: 2 High severity · 4 Medium severity

New issues introduced by this change (6)
Severity Finding
High severity src/​vs/​platform/​agentHost/​node/​devContainerAgentHostService.ts — This shared-process stop disconnects every relay for the workspace when any one renderer requests…
High severity src/​vs/​sessions/​contrib/​providers/​remoteAgentHost/​browser/​devContainerAgentHostService.ts — The existing connection is not acquired until _ensureActiveConnection resolves. During that…
Medium severity src/​vs/​platform/​agentHost/​node/​devContainerAgentHostService.ts — Lifecycle serialization and suspension are keyed by the raw workspaceFolder string, while the…
Medium severity src/​vs/​sessions/​contrib/​providers/​remoteAgentHost/​browser/​remoteAgentHostSessionsProvider.ts — Draft changes never trigger the idle check. If the last active session completes while a draft…
Medium severity src/​vs/​sessions/​contrib/​providers/​remoteAgentHost/​browser/​remoteAgentHostSessionsProvider.ts — When the last unarchived session is not the session carrying the detached-worktree handle, this…
Medium severity src/​vs/​sessions/​contrib/​providers/​remoteAgentHost/​browser/​remoteAgentHostSessionsProvider.tsEvent.toPromise is cancelable, but raceTimeout does not cancel the losing promise. On every…
What changed in this PR

Adds idle lifecycle management for session-owned Dev Containers, including reconnection and worktree-safe archival.

Changes:

  • Stops, resumes, and removes Dev Containers through serialized shared-process operations.
  • Retains disconnected providers and suppresses automatic reconnection.
  • Adds lifecycle, failure, draft, and archive tests.
File Description
remoteAgentHostSessionsProvider.test.ts Tests idle and archive behavior.
devContainerAgentHostService.test.ts Tests provider retention and reconnection.
REMOTE_AGENT_HOST_SESSIONS_PROVIDER.md Documents lifecycle contract.
devContainerAgentHostConnector.contribution.ts Exposes container operations.
remoteAgentHostSessionsProvider.ts Coordinates session and worktree lifecycle.
devContainerAgentHostService.ts Serializes renderer lifecycle operations.
baseAgentHostSessionsProvider.ts Exposes archive helpers.
sessions/​common/​devContainerAgentHostService.ts Extends connector API.
platform/​agentHost/​test/​node/​devContainerAgentHostService.test.ts Tests shared-process Docker behavior.
platform/​agentHost/​node/​devContainerAgentHostService.ts Implements Docker lifecycle management.
platform/​agentHost/​common/​devContainerAgentHost.ts Extends shared-process contracts.
Suppressed comments (2)

src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts:417

  • A session can leave the active state by entering SessionStatus.Error, but this condition only recognizes Completed. When the final running session fails, no stop is scheduled even though no session is active or waiting for input. Treat every transition from an active status to a non-active status as idle.
			becameIdle ||= previous !== undefined && isActiveSessionStatus(previous) && current === SessionStatus.Completed;

src/vs/sessions/contrib/providers/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts:332

  • A successful unarchive only restores the stopped state for Completed sessions. An archived session with SessionStatus.Error is also inactive, but unarchiving it leaves the replacement container running indefinitely. Gate on non-active status rather than exactly Completed.
			if (this.getSessions().find(session => session.sessionId === sessionId)?.status.get() === SessionStatus.Completed) {
				await this._stopDevContainerIfIdle();

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +433 to +436
const connectionIds = [...this._connectionWorkspaces]
.filter(([, workspace]) => workspace === workspaceFolder)
.map(([connectionId]) => connectionId);
await Promise.all(connectionIds.map(connectionId => this.disconnect(connectionId)));
Comment on lines +184 to +185
if (active) {
return raceCancellationError(this._ensureActiveConnection(key, active), token).then(() => this._acquireConnection(key, active));
Comment on lines +122 to +126
return this._containerOperations.queue(config.workspaceFolder, () => this._connect(config));
}

private async _connect(config: IDevContainerAgentHostConfig): Promise<IDevContainerAgentHostConnectResult> {
if (this._suspendedWorkspaces.has(config.workspaceFolder) && config.resume !== true) {
Comment on lines +246 to +248
if (this._devContainerLifecycle) {
this._register(this._onDidChangeSessionsImmediately(e => this._onDevContainerSessionsChanged(e)));
}
Comment on lines +301 to +302
await this._devContainerLifecycle.remove();
await this._setDetachedWorktreeArchived(sessionId, true);
Comment on lines +375 to +384
let timedOut = false;
const confirmation = raceTimeout(
Event.toPromise(Event.filter(connection.onDidAction, envelope =>
envelope.channel === backendUri.toString()
&& envelope.action.type === ActionType.SessionIsArchivedChanged
&& envelope.action.isArchived === archived
)),
DEV_CONTAINER_ARCHIVE_CONFIRMATION_TIMEOUT_MS,
() => timedOut = true,
);
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.

2 participants