Skip to content

Commit f47496b

Browse files
committed
Harden remote agent host session lifecycle
Written by Copilot
1 parent 2842110 commit f47496b

4 files changed

Lines changed: 44 additions & 2 deletions

File tree

src/vs/platform/agentHost/electron-browser/remoteAgentHostProtocolClient.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { AgentSubscriptionManager, type IAgentSubscription } from '../common/sta
2121
import { agentHostAuthority, fromAgentHostUri, toAgentHostUri } from '../common/agentHostUri.js';
2222
import type { IClientNotificationMap, ICommandMap } from '../common/state/protocol/messages.js';
2323
import type { IActionEnvelope, INotification, ISessionAction, ITerminalAction } from '../common/state/sessionActions.js';
24-
import { ISessionSummary, ROOT_STATE_URI, StateComponents, type IRootState } from '../common/state/sessionState.js';
24+
import { ISessionSummary, ROOT_STATE_URI, StateComponents, type IRootState, type ISessionState } from '../common/state/sessionState.js';
2525
import { PROTOCOL_VERSION } from '../common/state/sessionCapabilities.js';
2626
import { isJsonRpcNotification, isJsonRpcRequest, isJsonRpcResponse, type IJsonRpcResponse, type IProtocolMessage, type IStateSnapshot } from '../common/state/sessionProtocol.js';
2727
import { isClientTransport, type IProtocolTransport } from '../common/state/sessionTransport.js';
@@ -182,16 +182,30 @@ export class RemoteAgentHostProtocolClient extends Disposable implements IAgentC
182182
async createSession(config?: IAgentCreateSessionConfig): Promise<URI> {
183183
const provider = config?.provider ?? 'copilot';
184184
const session = AgentSession.uri(provider, generateUuid());
185+
const fork = this._toProtocolFork(config?.fork);
185186
await this._sendRequest('createSession', {
186187
session: session.toString(),
187188
provider,
188189
model: config?.model,
189190
workingDirectory: config?.workingDirectory ? fromAgentHostUri(config.workingDirectory).toString() : undefined,
191+
fork,
190192
config: config?.config,
191193
});
192194
return session;
193195
}
194196

197+
private _toProtocolFork(fork: IAgentCreateSessionConfig['fork']): { readonly session: string; readonly turnId: string } | undefined {
198+
if (!fork) {
199+
return undefined;
200+
}
201+
const sourceState = this.getSubscriptionUnmanaged<ISessionState>(StateComponents.Session, fork.session)?.value;
202+
const turnId = sourceState?.turns[fork.turnIndex]?.id;
203+
if (!turnId) {
204+
throw new Error(`Cannot fork: turn index ${fork.turnIndex} not found in protocol state for ${fork.session.toString()}`);
205+
}
206+
return { session: fork.session.toString(), turnId };
207+
}
208+
195209
async resolveSessionConfig(params: IAgentResolveSessionConfigParams): Promise<IResolveSessionConfigResult> {
196210
return this._sendRequest('resolveSessionConfig', {
197211
provider: params.provider,

src/vs/platform/agentHost/node/copilot/copilotAgent.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,9 +651,10 @@ export class CopilotAgent extends Disposable implements IAgent {
651651
}
652652
try {
653653
await this._gitService.removeWorktree(worktree.repositoryRoot, worktree.worktree);
654-
this._createdWorktrees.delete(sessionId);
655654
} catch (error) {
656655
this._logService.warn(`[Copilot:${sessionId}] Failed to remove worktree '${worktree.worktree.fsPath}': ${error instanceof Error ? error.message : String(error)}`);
656+
} finally {
657+
this._createdWorktrees.delete(sessionId);
657658
}
658659
}
659660

src/vs/sessions/contrib/remoteAgentHost/browser/remoteAgentHostSessionsProvider.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,12 @@ export class RemoteAgentHostSessionsProvider extends Disposable implements ISess
395395
this._onDidDisconnect.fire();
396396
this._connection = undefined;
397397
this._defaultDirectory = undefined;
398+
if (this._currentNewSession) {
399+
this._clearNewSessionConfig(this._currentNewSession.id);
400+
this._currentNewSession = undefined;
401+
}
402+
this._currentNewSessionStatus = undefined;
403+
this._selectedModelId = undefined;
398404

399405
if (this._sessionTypes.length > 0) {
400406
this._sessionTypes = [];

src/vs/sessions/contrib/remoteAgentHost/test/browser/remoteAgentHostSessionsProvider.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,27 @@ suite('RemoteAgentHostSessionsProvider', () => {
439439
});
440440
});
441441

442+
test('clearConnection clears pending new session config', () => {
443+
const provider = createProvider(disposables, connection);
444+
const workspace = {
445+
label: 'my-project',
446+
icon: { id: 'remote' },
447+
repositories: [{ uri: URI.parse('vscode-agent-host://auth/home/user/project'), workingDirectory: undefined, detail: undefined, baseBranchName: undefined, baseBranchProtected: undefined }],
448+
requiresWorkspaceTrust: false,
449+
};
450+
451+
const session = provider.createNewSession(workspace);
452+
provider.clearConnection();
453+
454+
assert.deepStrictEqual({
455+
resolved: provider.getSessionByResource(session.resource),
456+
config: provider.getSessionConfig(session.sessionId),
457+
}, {
458+
resolved: undefined,
459+
config: undefined,
460+
});
461+
});
462+
442463
test('createNewSession throws when no repository URI', () => {
443464
const provider = createProvider(disposables, connection);
444465
const workspace = { label: 'empty', icon: { id: 'remote' }, repositories: [], requiresWorkspaceTrust: false };

0 commit comments

Comments
 (0)