-
Notifications
You must be signed in to change notification settings - Fork 42k
Agent Host: surface remote connection state in sessions #334111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2858112
39db97d
1ee018b
880df7a
b0c6eef
c8303a9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -40,7 +40,7 @@ import { | |
| RemoteAgentHostEntryType, | ||
| } from '../common/remoteAgentHostService.js'; | ||
| import { computeReconnectDelay, hasExhaustedReconnectAttempts } from '../common/reconnectPolicy.js'; | ||
| import { NonReconnectableTransportError } from '../common/state/sessionTransport.js'; | ||
| import { AgentHostTransportFailureReason, NonReconnectableTransportError } from '../common/state/sessionTransport.js'; | ||
| import { AgentHostProtocolClient, InitialAuthenticationError } from './agentHostProtocolClient.js'; | ||
| import { WebSocketClientTransport } from './webSocketClientTransport.js'; | ||
| import { AGENT_HOST_LABEL_FORMATTER, AGENT_HOST_SCHEME, agentHostAuthority, normalizeRemoteAgentHostAddress } from '../common/agentHostUri.js'; | ||
|
|
@@ -51,7 +51,7 @@ import { agentsWindowAgentHostClientInfo, editorWindowAgentHostClientInfo } from | |
| /** Tracks a single remote connection through its lifecycle. */ | ||
| interface IConnectionEntry { | ||
| readonly store: DisposableStore; | ||
| readonly client: IRemoteAgentHostProtocolClient; | ||
| client?: IRemoteAgentHostProtocolClient; | ||
| /** | ||
| * Optional teardown for the shared-process tunnel that this entry's | ||
| * transport is using (SSH or dev-tunnels). Tracked separately from | ||
|
|
@@ -234,8 +234,8 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo | |
| result.push({ | ||
| address, | ||
| name: this._names.get(address) ?? address, | ||
| clientId: entry.client.clientId, | ||
| defaultDirectory: entry.client.defaultDirectory, | ||
| clientId: entry.client?.clientId, | ||
| defaultDirectory: entry.client?.defaultDirectory, | ||
| status: entry.status, | ||
| }); | ||
| } | ||
|
|
@@ -285,9 +285,9 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo | |
|
|
||
| async triggerServerUpgrade(address: string, method: string): Promise<IVscodeUpgradeResult> { | ||
| const normalized = normalizeRemoteAgentHostAddress(address); | ||
| const entry = this._entries.get(normalized); | ||
| if (!entry) { | ||
| throw new Error(`No remote agent host entry found for ${address}.`); | ||
| const client = this._entries.get(normalized)?.client; | ||
| if (!client) { | ||
| throw new Error(`No usable remote agent host client found for ${address}.`); | ||
| } | ||
| // The protocol client may be in any state: it might have completed | ||
| // the handshake (Connected) or it might be sitting on an | ||
|
|
@@ -296,7 +296,7 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo | |
| // method name the host advertised in its `_meta` payload; the | ||
| // server handler allows it pre-`initialize`. | ||
| const result = await raceTimeout( | ||
| entry.client.triggerVscodeUpgrade(method), | ||
| client.triggerVscodeUpgrade(method), | ||
| RemoteAgentHostService.UpgradeRequestTimeout, | ||
| ); | ||
| if (result === undefined) { | ||
|
|
@@ -365,6 +365,17 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo | |
| void this._connectTo(entryToReconnect, { userInitiated }); | ||
| } | ||
|
|
||
| /** | ||
| * Skips a protocol client's pending backoff, or starts a fresh user-initiated dial. | ||
| */ | ||
| reconnectNow(address: string): void { | ||
| const normalized = normalizeRemoteAgentHostAddress(address); | ||
| if (this._entries.get(normalized)?.client?.reconnectNow()) { | ||
| return; | ||
| } | ||
| this.reconnect(normalized, true); | ||
| } | ||
|
|
||
| async waitForConnection(address: string): Promise<IRemoteAgentHostConnectionInfo> { | ||
| if (this._store.isDisposed) { | ||
| throw new Error('Remote agent host service is disposed.'); | ||
|
|
@@ -438,11 +449,11 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo | |
| notifyConnectionClosed(address: string): void { | ||
| const normalized = normalizeRemoteAgentHostAddress(address); | ||
| const entry = this._entries.get(normalized); | ||
| if (entry) { | ||
| if (entry?.client) { | ||
| this._logService.info(`[RemoteAgentHost] notifyConnectionClosed: notifying protocol client for ${normalized}`); | ||
| entry.client.notifyTransportClosed(); | ||
| } else { | ||
| this._logService.info(`[RemoteAgentHost] notifyConnectionClosed: no entry found for ${normalized} (already removed?)`); | ||
| this._logService.info(`[RemoteAgentHost] notifyConnectionClosed: no active client found for ${normalized}`); | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -576,7 +587,27 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo | |
| createdConnection = await factory.createConnection(entryToCreate, options); | ||
| } catch (err) { | ||
| this._logService.error(`[RemoteAgentHost] Failed to create a connection to ${address}. Verify address and connectionToken`, err); | ||
| // A factory can fail before any client exists — a stopped WSL distro is | ||
| // rejected by its precondition check, never reaching the handshake below. | ||
| // Retain the entry so its status remains visible to consumers. | ||
| const disconnectReason = err instanceof NonReconnectableTransportError ? err.reason : AgentHostTransportFailureReason.Unknown; | ||
| this._entries.set(address, { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. AI Review: |
||
| store: new DisposableStore(), | ||
| connected: false, | ||
| status: RemoteAgentHostConnectionStatus.disconnectedBecause(disconnectReason), | ||
| reconnectTransfersTransportOwnership: false, | ||
| }); | ||
|
connor4312 marked this conversation as resolved.
|
||
| this._rejectPendingConnectionWait(address, err); | ||
| // Clear the in-flight marker before notifying. A consumer may dial again | ||
| // from this notification — an automatic start does — and `reconnect` | ||
| // joins a pending dial rather than racing it. Leaving the marker set | ||
| // would join *this* dial, which has already failed, so the new request | ||
| // would never connect and its `waitForConnection` would never settle. | ||
| // `_connectTo` clears by identity, so a fresh dial started here survives. | ||
| this._pendingConnects.delete(address); | ||
| // Nothing else reports this failure: no entry was created, so consumers | ||
| // only learn the address became unavailable from this notification. | ||
| this._onDidChangeConnections.fire(); | ||
| if (!isTerminalConnectError(err) && !this._store.isDisposed && this._remoteAgentHostsEnabled.get()) { | ||
| this._scheduleReconnect(address, entryToCreate.connectionToken); | ||
| } | ||
|
|
@@ -611,13 +642,15 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo | |
| // current entry for this address is still the one we created. | ||
| const isCurrentEntry = () => this._entries.get(address) === entry; | ||
|
|
||
| store.add(client.onDidClose(() => { | ||
| store.add(client.onDidClose(reason => { | ||
| if (!isCurrentEntry()) { | ||
| return; | ||
| } | ||
| this._logService.warn(`[RemoteAgentHost] Connection closed: ${address}`); | ||
| entry.connected = false; | ||
| entry.status = RemoteAgentHostConnectionStatus.disconnected; | ||
| entry.status = RemoteAgentHostConnectionStatus.disconnectedBecause(reason ?? AgentHostTransportFailureReason.Unknown); | ||
| entry.client = undefined; | ||
| disposeEntry(entry); | ||
| this._onDidChangeConnections.fire(); | ||
| // Schedule reconnect if the address is still configured. This is | ||
| // the "fatal" path — the protocol client already gave up its own | ||
|
|
@@ -628,14 +661,23 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo | |
|
|
||
| // Surface self-healing transport drops separately so outer reconnect | ||
| // loops do not replace the protocol client while it restores itself. | ||
| store.add(client.onDidScheduleReconnect(() => { | ||
| // The client stays `reconnecting` across backoff rounds, so only this | ||
| // event reports that the deadline moved. | ||
| if (!isCurrentEntry() || entry.status.kind !== 'reconnecting') { | ||
| return; | ||
| } | ||
| entry.status = RemoteAgentHostConnectionStatus.reconnectingUntil(client.nextReconnectAt); | ||
| this._onDidChangeConnections.fire(); | ||
| })); | ||
| store.add(client.onDidChangeConnectionState(state => { | ||
| if (!isCurrentEntry()) { | ||
| return; | ||
| } | ||
| switch (state) { | ||
| case 'reconnecting': | ||
| entry.connected = false; | ||
| entry.status = RemoteAgentHostConnectionStatus.reconnecting; | ||
| entry.status = RemoteAgentHostConnectionStatus.reconnectingUntil(client.nextReconnectAt); | ||
| this._onDidChangeConnections.fire(); | ||
| break; | ||
| case 'connected': | ||
|
|
@@ -715,9 +757,10 @@ export class RemoteAgentHostService extends Disposable implements IRemoteAgentHo | |
| return; | ||
| } | ||
|
|
||
| entry.status = RemoteAgentHostConnectionStatus.disconnected; | ||
| // Clean up the failed entry | ||
| this._entries.delete(address); | ||
| const disconnectReason = err instanceof NonReconnectableTransportError ? err.reason : AgentHostTransportFailureReason.Unknown; | ||
| entry.status = RemoteAgentHostConnectionStatus.disconnectedBecause(disconnectReason); | ||
| entry.client = undefined; | ||
| // Clean up the failed client while retaining its entry and status. | ||
| disposeEntry(entry); | ||
| this._rejectPendingConnectionWait(address, err); | ||
| this._onDidChangeConnections.fire(); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AI Review:
AgentHostProtocolClient.reconnectNow()returns false once its backoff timer fires and the reconnect attempt is in flight. The service status is not refreshed immediately, so Try Now can remain clickable until the separate countdown tick; clicking then falls through toreconnect(), disposes the state-preserving client, and starts a fresh one. When an entry has a client, invoke its method and return regardless of the boolean result; use the fresh-dial fallback only whenclientis absent.