@@ -21,12 +21,11 @@ import { localize } from '../../../../nls.js';
2121import { IAgentPluginManager , ISyncedCustomization } from '../../common/agentPluginManager.js' ;
2222import { AgentHostSessionConfigBranchNameHintKey , AgentSession , IAgent , IAgentAttachment , IAgentCreateSessionConfig , IAgentCreateSessionResult , IAgentDescriptor , IAgentMessageEvent , IAgentModelInfo , IAgentProgressEvent , IAgentResolveSessionConfigParams , IAgentSessionConfigCompletionsParams , IAgentSessionMetadata , IAgentSessionProjectInfo , IAgentSubagentStartedEvent , IAgentToolCompleteEvent , IAgentToolStartEvent } from '../../common/agentService.js' ;
2323import type { IResolveSessionConfigResult , ISessionConfigCompletionsResult } from '../../common/state/protocol/commands.js' ;
24- import { ISessionDataService } from '../../common/sessionDataService.js' ;
24+ import { ISessionDataService , SESSION_DB_FILENAME } from '../../common/sessionDataService.js' ;
2525import { CustomizationStatus , ICustomizationRef , SessionInputResponseKind , type ISessionInputAnswer , type IPendingMessage , type PolicyState } from '../../common/state/sessionState.js' ;
2626import { CopilotAgentSession , SessionWrapperFactory } from './copilotAgentSession.js' ;
2727import { parsedPluginsEqual , toSdkCustomAgents , toSdkHooks , toSdkMcpServers , toSdkSkillDirectories } from './copilotPluginConverters.js' ;
2828import { CopilotSessionWrapper } from './copilotSessionWrapper.js' ;
29- import { forkCopilotSessionOnDisk , getCopilotDataDir , truncateCopilotSessionOnDisk } from './copilotAgentForking.js' ;
3029import { IProtectedResourceMetadata } from '../../common/state/protocol/state.js' ;
3130import { IAgentHostGitService } from '../agentHostGitService.js' ;
3231import { IAgentHostTerminalManager } from '../agentHostTerminalManager.js' ;
@@ -236,27 +235,50 @@ export class CopilotAgent extends Disposable implements IAgent {
236235 const client = await this . _ensureClient ( ) ;
237236 const parsedPlugins = await this . _plugins . getAppliedPlugins ( ) ;
238237
239- // When forking, we manipulate the CLI's on-disk data and then resume
240- // instead of creating a fresh session via the SDK.
238+ // When forking, use the SDK's sessions.fork RPC.
241239 if ( config ?. fork ) {
242240 const sourceSessionId = AgentSession . id ( config . fork . session ) ;
243- const newSessionId = config . session ? AgentSession . id ( config . session ) : generateUuid ( ) ;
244241
245242 // Serialize against the source session to prevent concurrent
246- // modifications while we read its on-disk data .
243+ // modifications while we read its state .
247244 return this . _sessionSequencer . queue ( sourceSessionId , async ( ) => {
248- this . _logService . info ( `[Copilot] Forking session ${ sourceSessionId } at index ${ config . fork ! . turnIndex } → ${ newSessionId } ` ) ;
245+ this . _logService . info ( `[Copilot] Forking session ${ sourceSessionId } at turnId= ${ config . fork ! . turnId } ` ) ;
249246
250- // Ensure the source session is loaded so on-disk data is available
251- if ( ! this . _sessions . has ( sourceSessionId ) ) {
252- await this . _resumeSession ( sourceSessionId ) ;
253- }
247+ // Ensure the source session is loaded so we can read its event IDs
248+ const sourceEntry = this . _sessions . get ( sourceSessionId ) ?? await this . _resumeSession ( sourceSessionId ) ;
254249
255- const copilotDataDir = getCopilotDataDir ( ) ;
256- await forkCopilotSessionOnDisk ( copilotDataDir , sourceSessionId , newSessionId , config . fork ! . turnIndex ) ;
250+ // Look up the SDK event ID for the turn *after* the fork point.
251+ // toEventId is exclusive — events before it are included.
252+ // If there's no next turn, omit toEventId to include all events.
253+ const toEventId = await sourceEntry . getNextTurnEventId ( config . fork ! . turnId ) ;
254+
255+ const forkResult = await client . rpc . sessions . fork ( {
256+ sessionId : sourceSessionId ,
257+ ...( toEventId ? { toEventId } : { } ) ,
258+ } ) ;
259+ const newSessionId = forkResult . sessionId ;
260+
261+ // Copy the source session's database file so the forked session
262+ // inherits turn event IDs and file-edit snapshots.
263+ const sourceDbDir = this . _sessionDataService . getSessionDataDir ( config . fork ! . session ) ;
264+ const targetDbDir = this . _sessionDataService . getSessionDataDirById ( newSessionId ) ;
265+ const sourceDbPath = URI . joinPath ( sourceDbDir , SESSION_DB_FILENAME ) ;
266+ const targetDbPath = URI . joinPath ( targetDbDir , SESSION_DB_FILENAME ) ;
267+ try {
268+ await fs . mkdir ( targetDbDir . fsPath , { recursive : true } ) ;
269+ await fs . copyFile ( sourceDbPath . fsPath , targetDbPath . fsPath ) ;
270+ } catch ( err ) {
271+ this . _logService . warn ( `[Copilot] Failed to copy session database for fork: ${ err instanceof Error ? err . message : String ( err ) } ` ) ;
272+ }
257273
258274 // Resume the forked session so the SDK loads the forked history
259275 const agentSession = await this . _resumeSession ( newSessionId ) ;
276+
277+ // Remap turn IDs to match the new protocol turn IDs
278+ if ( config . fork ! . turnIdMapping ) {
279+ await agentSession . remapTurnIds ( config . fork ! . turnIdMapping ) ;
280+ }
281+
260282 const session = agentSession . sessionUri ;
261283 this . _logService . info ( `[Copilot] Forked session created: ${ session . toString ( ) } ` ) ;
262284 const project = await projectFromCopilotContext ( { cwd : config . workingDirectory ?. fsPath } , this . _gitService ) ;
@@ -448,39 +470,33 @@ export class CopilotAgent extends Disposable implements IAgent {
448470 } ) ;
449471 }
450472
451- async truncateSession ( session : URI , turnIndex ?: number ) : Promise < void > {
473+ async truncateSession ( session : URI , turnId ?: string ) : Promise < void > {
452474 const sessionId = AgentSession . id ( session ) ;
453475 await this . _sessionSequencer . queue ( sessionId , async ( ) => {
454- this . _logService . info ( `[Copilot:${ sessionId } ] Truncating session${ turnIndex !== undefined ? ` at index ${ turnIndex } ` : ' (all turns)' } ` ) ;
455-
456- const keepUpToTurnIndex = turnIndex ?? - 1 ;
457-
458- // Destroy the SDK session first and wait for cleanup to complete,
459- // ensuring on-disk data (events.jsonl, locks) is released before
460- // we modify it. Then dispose the wrapper.
461- const entry = this . _sessions . get ( sessionId ) ;
462- if ( entry ) {
463- await entry . destroySession ( ) ;
476+ this . _logService . info ( `[Copilot:${ sessionId } ] Truncating session${ turnId !== undefined ? ` at turnId=${ turnId } ` : ' (all turns)' } ` ) ;
477+
478+ // Ensure the session is loaded so we can use the SDK RPC
479+ const entry = this . _sessions . get ( sessionId ) ?? await this . _resumeSession ( sessionId ) ;
480+
481+ // Look up the SDK event ID for the truncation boundary.
482+ // The protocol semantics: turnId is the last turn to KEEP.
483+ // The SDK semantics: eventId and all events after it are removed.
484+ // So we need the event ID of the *next* turn after turnId.
485+ // For "remove all", we need the first turn's event ID.
486+ let eventId : string | undefined ;
487+ if ( turnId ) {
488+ eventId = await entry . getNextTurnEventId ( turnId ) ;
489+ } else {
490+ eventId = await entry . getFirstTurnEventId ( ) ;
464491 }
465- this . _sessions . deleteAndDispose ( sessionId ) ;
466-
467- const copilotDataDir = getCopilotDataDir ( ) ;
468- await truncateCopilotSessionOnDisk ( copilotDataDir , sessionId , keepUpToTurnIndex ) ;
469492
470- // Resume the session from the modified on-disk data
471- await this . _resumeSession ( sessionId ) ;
472- this . _logService . info ( `[Copilot:${ sessionId } ] Session truncated and resumed` ) ;
473- } ) ;
474- }
475-
476- async forkSession ( sourceSession : URI , newSessionId : string , turnIndex : number ) : Promise < void > {
477- const sourceSessionId = AgentSession . id ( sourceSession ) ;
478- await this . _sessionSequencer . queue ( sourceSessionId , async ( ) => {
479- this . _logService . info ( `[Copilot] Forking session ${ sourceSessionId } at index ${ turnIndex } → ${ newSessionId } ` ) ;
493+ if ( eventId ) {
494+ await entry . truncateAtEventId ( eventId , turnId ) ;
495+ } else {
496+ this . _logService . info ( `[Copilot:${ sessionId } ] No event ID found for truncation, nothing to truncate` ) ;
497+ }
480498
481- const copilotDataDir = getCopilotDataDir ( ) ;
482- await forkCopilotSessionOnDisk ( copilotDataDir , sourceSessionId , newSessionId , turnIndex ) ;
483- this . _logService . info ( `[Copilot] Forked session ${ newSessionId } created on disk` ) ;
499+ this . _logService . info ( `[Copilot:${ sessionId } ] Session truncated` ) ;
484500 } ) ;
485501 }
486502
0 commit comments