Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/vscode-node/chatSessionWorktreeCheckpointServiceImpl.ts
13399 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { promises as fs } from 'fs';
7
8
import { Uri } from 'vscode';
9
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
10
import { IGitService, RepoContext } from '../../../platform/git/common/gitService';
11
import { ILogService } from '../../../platform/log/common/logService';
12
import { Disposable } from '../../../util/vs/base/common/lifecycle';
13
import * as path from '../../../util/vs/base/common/path';
14
import { generateUuid } from '../../../util/vs/base/common/uuid';
15
import { IAgentSessionsWorkspace } from '../common/agentSessionsWorkspace';
16
import { IChatSessionMetadataStore } from '../common/chatSessionMetadataStore';
17
import { IChatSessionWorkspaceFolderService } from '../common/chatSessionWorkspaceFolderService';
18
import { IChatSessionWorktreeCheckpointService } from '../common/chatSessionWorktreeCheckpointService';
19
import { IChatSessionWorktreeService } from '../common/chatSessionWorktreeService';
20
import { buildTempIndexEnv, getUncommittedFilePaths } from '../../../platform/git/vscode-node/utils';
21
22
const CHECKPOINT_REF_PREFIX = 'refs/sessions/';
23
24
function getCheckpointRef(sessionId: string, turnNumber: number): string {
25
return `${CHECKPOINT_REF_PREFIX}${sessionId}/checkpoints/turn/${turnNumber}`;
26
}
27
28
export class ChatSessionWorktreeCheckpointService extends Disposable implements IChatSessionWorktreeCheckpointService {
29
declare _serviceBrand: undefined;
30
31
constructor(
32
@IAgentSessionsWorkspace private readonly agentSessionsWorkspace: IAgentSessionsWorkspace,
33
@IChatSessionMetadataStore private readonly metadataStore: IChatSessionMetadataStore,
34
@IChatSessionWorkspaceFolderService private readonly workspaceFolderService: IChatSessionWorkspaceFolderService,
35
@IChatSessionWorktreeService private readonly worktreeService: IChatSessionWorktreeService,
36
@IGitService private readonly gitService: IGitService,
37
@ILogService private readonly logService: ILogService,
38
@IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext,
39
) {
40
super();
41
}
42
43
async handleRequest(sessionId: string): Promise<void> {
44
if (!this._getSessionCheckpointSupport()) {
45
this.logService.trace('[ChatSessionWorktreeCheckpointService][handleRequest] Session does not support checkpoints, skipping baseline checkpoint creation');
46
return;
47
}
48
49
const repositoryUri = await this._getSessionRepository(sessionId);
50
const repository = repositoryUri ? await this.gitService.getRepository(repositoryUri) : undefined;
51
52
if (!repository || !repository.headCommitHash) {
53
this.logService.warn(`[ChatSessionWorktreeCheckpointService][handleRequest] No repository found for session ${sessionId}, skipping baseline checkpoint creation`);
54
return;
55
}
56
57
// Initialize checkpoint state and capture baseline checkpoint
58
const checkpointRef = await this._createCheckpoint(sessionId, repository, 0);
59
if (!checkpointRef) {
60
return;
61
}
62
63
// Update session metadata
64
const worktreeProperties = await this.worktreeService.getWorktreeProperties(sessionId);
65
if (!worktreeProperties || typeof worktreeProperties === 'string' || worktreeProperties.version === 1) {
66
this.logService.trace(`[ChatSessionWorktreeCheckpointService][handleRequest] Session ${sessionId} does not use a git worktree, skipping checkpoint metadata update`);
67
return;
68
}
69
70
await this.worktreeService.setWorktreeProperties(sessionId, {
71
...worktreeProperties,
72
firstCheckpointRef: checkpointRef,
73
baseCheckpointRef: checkpointRef,
74
lastCheckpointRef: checkpointRef
75
});
76
}
77
78
async handleRequestCompleted(sessionId: string, requestId: string): Promise<void> {
79
if (!this._getSessionCheckpointSupport()) {
80
this.logService.trace('[ChatSessionWorktreeCheckpointService][handleRequestCompleted] Session does not support checkpoints, skipping post-turn checkpoint');
81
return;
82
}
83
84
const repositoryUri = await this._getSessionRepository(sessionId);
85
const repository = repositoryUri ? await this.gitService.getRepository(repositoryUri) : undefined;
86
87
if (!repository || !repository.headCommitHash) {
88
this.logService.warn(`[ChatSessionWorktreeCheckpointService][handleRequestCompleted] No repository found for session ${sessionId}, skipping post-turn checkpoint`);
89
return;
90
}
91
92
const parentCheckpointRef = await this._getLatestCheckpointRef(sessionId);
93
if (!parentCheckpointRef) {
94
this.logService.warn(`[ChatSessionWorktreeCheckpointService][handleRequestCompleted] No existing checkpoint ref found for session ${sessionId} on request completion, skipping post-turn checkpoint`);
95
return;
96
}
97
98
// Create checkpoint
99
const currentTurn = parseInt(parentCheckpointRef.split('/').pop() ?? '0') + 1;
100
const checkpointRef = await this._createCheckpoint(sessionId, repository, currentTurn, parentCheckpointRef);
101
if (!checkpointRef) {
102
return;
103
}
104
105
const worktreeProperties = await this.worktreeService.getWorktreeProperties(sessionId);
106
if (worktreeProperties && typeof worktreeProperties !== 'string' && worktreeProperties.version === 2) {
107
// Worktree isolation mode
108
await this.worktreeService.setWorktreeProperties(sessionId, {
109
...worktreeProperties,
110
changes: undefined,
111
lastCheckpointRef: checkpointRef
112
});
113
}
114
115
// Update request metadata with new checkpoint ref
116
await this.metadataStore.updateRequestDetails(sessionId, [{ vscodeRequestId: requestId, checkpointRef }]);
117
}
118
119
private async _getSessionRepository(sessionId: string): Promise<Uri | undefined> {
120
const worktreeProperties = await this.worktreeService.getWorktreeProperties(sessionId);
121
if (worktreeProperties) {
122
// Worktree isolation mode
123
if (typeof worktreeProperties === 'string' || worktreeProperties.version === 1) {
124
return undefined;
125
}
126
127
return Uri.file(worktreeProperties.worktreePath);
128
}
129
130
// Workspace isolation mode
131
return this.workspaceFolderService.getSessionWorkspaceFolder(sessionId);
132
}
133
134
private async _getLatestCheckpointRef(sessionId: string): Promise<string | undefined> {
135
const repositoryUri = await this._getSessionRepository(sessionId);
136
const repository = repositoryUri ? await this.gitService.getRepository(repositoryUri) : undefined;
137
if (!repository) {
138
return undefined;
139
}
140
141
try {
142
const refPattern = `${CHECKPOINT_REF_PREFIX}${sessionId}/checkpoints/turn/`;
143
const refs = await this.gitService.exec(repository.rootUri, [
144
'for-each-ref', '--sort=-committerdate', '--format=%(refname)', refPattern]);
145
146
return refs ? refs.split('\n')[0] : undefined;
147
} catch (error) {
148
this.logService.error(`[ChatSessionWorktreeCheckpointService][_getLatestCheckpointRef] Failed to get latest checkpoint ref for session ${sessionId}: `, error);
149
return undefined;
150
}
151
}
152
153
private _getSessionCheckpointSupport(): boolean {
154
return this.agentSessionsWorkspace.isAgentSessionsWorkspace;
155
}
156
157
async handleAdditionalWorktreesRequest(sessionId: string): Promise<void> {
158
if (!this._getSessionCheckpointSupport()) {
159
return;
160
}
161
162
const additionalProps = await this.worktreeService.getAdditionalWorktreeProperties(sessionId);
163
for (const props of additionalProps) {
164
if (typeof props === 'string' || props.version === 1) {
165
continue;
166
}
167
const repoUri = Uri.file(props.worktreePath);
168
const repository = await this.gitService.getRepository(repoUri);
169
if (!repository || !repository.headCommitHash) {
170
this.logService.warn(`[ChatSessionWorktreeCheckpointService][handleAdditionalWorktreesRequest] No repository found for additional worktree ${props.worktreePath}`);
171
continue;
172
}
173
await this._createCheckpoint(sessionId, repository, 0);
174
}
175
}
176
177
async handleAdditionalWorktreesRequestCompleted(sessionId: string, requestId: string): Promise<void> {
178
if (!this._getSessionCheckpointSupport()) {
179
return;
180
}
181
182
const additionalProps = await this.worktreeService.getAdditionalWorktreeProperties(sessionId);
183
const additionalCheckpointRefs: { [folderPath: string]: string } = {};
184
185
await Promise.allSettled(additionalProps.map(async (props) => {
186
if (typeof props === 'string' || props.version === 1) {
187
return;
188
}
189
const repoUri = Uri.file(props.worktreePath);
190
const repository = await this.gitService.getRepository(repoUri);
191
if (!repository || !repository.headCommitHash) {
192
return;
193
}
194
195
const parentCheckpointRef = await this._getLatestCheckpointRef(sessionId);
196
const currentTurn = parentCheckpointRef ? parseInt(parentCheckpointRef.split('/').pop() ?? '0') + 1 : 0;
197
const checkpointRef = await this._createCheckpoint(sessionId, repository, currentTurn, parentCheckpointRef);
198
if (checkpointRef) {
199
additionalCheckpointRefs[props.repositoryPath] = checkpointRef;
200
}
201
}));
202
203
if (Object.keys(additionalCheckpointRefs).length > 0) {
204
await this.metadataStore.updateRequestDetails(sessionId, [{ vscodeRequestId: requestId, additionalCheckpointRefs }]);
205
}
206
}
207
208
private async _createCheckpoint(sessionId: string, repository: RepoContext, turnNumber: number, parentCheckpointRef?: string): Promise<string | undefined> {
209
const repositoryUri = repository.rootUri;
210
211
const tmpDirName = `vscode-sessions-${sessionId}-${generateUuid()}`;
212
const checkpointIndexFile = path.join(this.extensionContext.globalStorageUri.fsPath, tmpDirName, `checkpoint.index`);
213
const pathspecFile = path.join(this.extensionContext.globalStorageUri.fsPath, tmpDirName, `pathspec.txt`);
214
215
const env = buildTempIndexEnv(repository, checkpointIndexFile);
216
217
try {
218
// Create temp index file directory
219
await fs.mkdir(path.dirname(checkpointIndexFile), { recursive: true });
220
221
// Populate temp index from HEAD
222
await this.gitService.exec(repositoryUri, ['read-tree', 'HEAD'], env);
223
224
// Stage entire working directory into temp index
225
const uncommittedFilePaths = getUncommittedFilePaths(repository);
226
await fs.writeFile(pathspecFile, uncommittedFilePaths.join('\n'), 'utf8');
227
await this.gitService.exec(repositoryUri, ['add', '-A', `--pathspec-from-file=${pathspecFile}`], env);
228
229
// Write the temp index as a tree object
230
const treeOid = await this.gitService.exec(repositoryUri, ['write-tree'], env);
231
232
// Resolve parent checkpoint ref
233
const parentCommitOid = parentCheckpointRef
234
? await this.gitService.exec(repositoryUri, ['rev-parse', parentCheckpointRef])
235
: undefined;
236
237
// Create a commit pointing to the tree, chained to the previous checkpoint
238
const commitTreeArgs = ['commit-tree', treeOid, ...(parentCommitOid ? ['-p', parentCommitOid] : []), '-m', `Session ${sessionId} - checkpoint turn ${turnNumber}`];
239
const commitOid = await this.gitService.exec(repositoryUri, commitTreeArgs);
240
241
// Point a new ref at the commit
242
const checkpointRef = getCheckpointRef(sessionId, turnNumber);
243
await this.gitService.exec(repositoryUri, ['update-ref', checkpointRef, commitOid]);
244
245
this.logService.trace(`[ChatSessionWorktreeCheckpointService][_createCheckpoint] Captured checkpoint turn ${turnNumber} for session ${sessionId} at ${checkpointRef}`);
246
return checkpointRef;
247
} catch (error) {
248
this.logService.error(`[ChatSessionWorktreeCheckpointService][_createCheckpoint] Failed to capture checkpoint turn ${turnNumber} for session ${sessionId}: `, error);
249
return undefined;
250
} finally {
251
try {
252
await fs.rm(path.dirname(checkpointIndexFile), { recursive: true, force: true });
253
} catch (error) {
254
this.logService.error(`[ChatSessionWorktreeCheckpointService][_createCheckpoint] Error while cleaning up temp index file for session ${sessionId}: ${error}`);
255
}
256
}
257
}
258
}
259
260