@@ -70,6 +70,7 @@ export class CodexAcpServer implements acp.Agent {
7070 private readonly sessions : Map < string , SessionState > ;
7171 private readonly pendingMcpStartupSessions : Map < string , PendingMcpStartupSession > ;
7272 private readonly closingSessions : Set < string > ;
73+ private readonly closeSessionPromises : Map < string , Promise < void > > ;
7374
7475 constructor (
7576 connection : acp . AgentSideConnection ,
@@ -80,6 +81,7 @@ export class CodexAcpServer implements acp.Agent {
8081 this . sessions = new Map ( ) ;
8182 this . pendingMcpStartupSessions = new Map ( ) ;
8283 this . closingSessions = new Set ( ) ;
84+ this . closeSessionPromises = new Map ( ) ;
8385 this . connection = connection ;
8486 this . codexAcpClient = codexAcpClient ;
8587 this . defaultAuthRequest = defaultAuthRequest ?? null ;
@@ -246,24 +248,40 @@ export class CodexAcpServer implements acp.Agent {
246248
247249 async closeSession ( params : acp . CloseSessionRequest ) : Promise < acp . CloseSessionResponse > {
248250 logger . log ( "Closing session..." , { sessionId : params . sessionId } ) ;
249- const sessionState = this . getSessionState ( params . sessionId ) ;
250- this . closingSessions . add ( params . sessionId ) ;
251+ const existingClose = this . closeSessionPromises . get ( params . sessionId ) ;
252+ if ( existingClose ) {
253+ await existingClose ;
254+ logger . log ( "Session close already completed" , { sessionId : params . sessionId } ) ;
255+ return { } ;
256+ }
251257
258+ const closePromise = this . closeSessionInternal ( params . sessionId ) ;
259+ this . closeSessionPromises . set ( params . sessionId , closePromise ) ;
252260 try {
253- const activeTurnId = await this . resolveActiveTurnId ( sessionState ) ;
254- await this . runWithProcessCheck ( ( ) =>
255- this . codexAcpClient . closeSession ( params . sessionId , activeTurnId )
256- ) ;
257- this . forgetSession ( params . sessionId ) ;
258- } catch ( err ) {
259- this . closingSessions . delete ( params . sessionId ) ;
260- throw err ;
261+ await closePromise ;
262+ } finally {
263+ this . closeSessionPromises . delete ( params . sessionId ) ;
261264 }
262265
263266 logger . log ( "Session closed" , { sessionId : params . sessionId } ) ;
264267 return { } ;
265268 }
266269
270+ private async closeSessionInternal ( sessionId : string ) : Promise < void > {
271+ logger . log ( "Closing session internals..." , { sessionId} ) ;
272+ const sessionState = this . getSessionState ( sessionId ) ;
273+ this . closingSessions . add ( sessionId ) ;
274+
275+ try {
276+ const activeTurnId = await this . resolveActiveTurnId ( sessionState ) ;
277+ await this . runWithProcessCheck ( ( ) =>
278+ this . codexAcpClient . closeSession ( sessionId , activeTurnId )
279+ ) ;
280+ } finally {
281+ this . forgetSession ( sessionId ) ;
282+ }
283+ }
284+
267285 async newSession (
268286 params : acp . NewSessionRequest ,
269287 ) : Promise < acp . NewSessionResponse > {
@@ -305,6 +323,9 @@ export class CodexAcpServer implements acp.Agent {
305323 } ) ;
306324 const sessionState = this . sessions . get ( _params . sessionId ) ;
307325 if ( ! sessionState ) throw new Error ( `Session ${ _params . sessionId } not found` ) ;
326+ if ( this . closingSessions . has ( _params . sessionId ) ) {
327+ throw RequestError . invalidRequest ( "Session is closing" ) ;
328+ }
308329
309330 const newMode = AgentMode . find ( _params . modeId ) ;
310331 if ( ! newMode ) {
@@ -321,6 +342,9 @@ export class CodexAcpServer implements acp.Agent {
321342 } ) ;
322343 const sessionState = this . sessions . get ( params . sessionId ) ;
323344 if ( ! sessionState ) throw new Error ( `Session ${ params . sessionId } not found` ) ;
345+ if ( this . closingSessions . has ( params . sessionId ) ) {
346+ throw RequestError . invalidRequest ( "Session is closing" ) ;
347+ }
324348
325349 const requestedModelId = ModelId . fromString ( params . modelId ) ;
326350 const requestedModelName = requestedModelId . model ;
@@ -685,10 +709,8 @@ export class CodexAcpServer implements acp.Agent {
685709 }
686710 }
687711
688- private ensureSessionIsActive ( sessionId : string , requireTrackedSession : boolean = false ) : void {
689- if ( this . closingSessions . has ( sessionId ) || ( requireTrackedSession && ! this . sessions . has ( sessionId ) ) ) {
690- throw RequestError . invalidRequest ( "Session is closing" ) ;
691- }
712+ private isSessionClosedOrClosing ( sessionId : string ) : boolean {
713+ return this . closingSessions . has ( sessionId ) || ! this . sessions . has ( sessionId ) ;
692714 }
693715
694716 private forgetSession ( sessionId : string ) : void {
@@ -781,8 +803,9 @@ export class CodexAcpServer implements acp.Agent {
781803 prompt : params . prompt ,
782804 } ) ;
783805 const sessionState = this . getSessionState ( params . sessionId ) ;
784- const requireTrackedSession = this . sessions . has ( params . sessionId ) ;
785- this . ensureSessionIsActive ( params . sessionId , requireTrackedSession ) ;
806+ if ( this . isSessionClosedOrClosing ( params . sessionId ) ) {
807+ return this . buildCancelledPromptResponse ( sessionState ) ;
808+ }
786809 sessionState . currentTurnId = null ;
787810 sessionState . lastTokenUsage = null ;
788811
@@ -800,18 +823,25 @@ export class CodexAcpServer implements acp.Agent {
800823 } ,
801824 approvalHandler ,
802825 elicitationHandler ) ;
803- this . ensureSessionIsActive ( params . sessionId , requireTrackedSession ) ;
826+ if ( this . isSessionClosedOrClosing ( params . sessionId ) ) {
827+ return this . buildCancelledPromptResponse ( sessionState ) ;
828+ }
804829
805830 if ( await this . availableCommands . tryHandle ( params . prompt , sessionState ) ) {
806831 logger . log ( "Prompt handled by a command" ) ;
832+ if ( this . isSessionClosedOrClosing ( params . sessionId ) ) {
833+ return this . buildCancelledPromptResponse ( sessionState ) ;
834+ }
807835 return {
808836 stopReason : "end_turn" ,
809837 usage : this . buildPromptUsage ( sessionState . lastTokenUsage ) ,
810838 _meta : this . buildQuotaMeta ( sessionState ) ,
811839 } ;
812840 }
813841
814- this . ensureSessionIsActive ( params . sessionId , requireTrackedSession ) ;
842+ if ( this . isSessionClosedOrClosing ( params . sessionId ) ) {
843+ return this . buildCancelledPromptResponse ( sessionState ) ;
844+ }
815845
816846 const modelId = ModelId . fromString ( sessionState . currentModelId ) ;
817847 const modelLacksReasoning = sessionState . supportedReasoningEfforts . length > 0
@@ -832,8 +862,20 @@ export class CodexAcpServer implements acp.Agent {
832862 const turnIdPromise = this . runWithProcessCheck (
833863 ( ) => this . codexAcpClient . startPrompt ( params , agentMode , modelId , disableSummary , sessionState . cwd )
834864 ) ;
865+ turnIdPromise . catch ( ( ) => {
866+ // The prompt path awaits this promise, but close also reads currentTurnId.
867+ // Attach a handler immediately so future readers cannot create an unhandled rejection.
868+ } ) ;
835869 sessionState . currentTurnId = turnIdPromise ;
836- const turnId = await turnIdPromise ;
870+ let turnId : string ;
871+ try {
872+ turnId = await turnIdPromise ;
873+ } catch ( err ) {
874+ if ( this . isSessionClosedOrClosing ( params . sessionId ) ) {
875+ return this . buildCancelledPromptResponse ( sessionState ) ;
876+ }
877+ throw err ;
878+ }
837879 sessionState . currentTurnId = turnId ;
838880
839881 const turnCompleted = await this . runWithProcessCheck (
@@ -854,11 +896,7 @@ export class CodexAcpServer implements acp.Agent {
854896 }
855897 } ) ;
856898 }
857- return {
858- stopReason : "cancelled" ,
859- usage : this . buildPromptUsage ( sessionState . lastTokenUsage ) ,
860- _meta : this . buildQuotaMeta ( sessionState ) ,
861- } ;
899+ return this . buildCancelledPromptResponse ( sessionState ) ;
862900 }
863901
864902 const error = eventHandler . getFailure ( )
@@ -873,6 +911,9 @@ export class CodexAcpServer implements acp.Agent {
873911 _meta : this . buildQuotaMeta ( sessionState ) ,
874912 } ;
875913 } catch ( err ) {
914+ if ( this . isSessionClosedOrClosing ( params . sessionId ) ) {
915+ return this . buildCancelledPromptResponse ( sessionState ) ;
916+ }
876917 logger . error ( `Prompt for session ${ params . sessionId } failed` , err ) ;
877918 throw err ;
878919 } finally {
@@ -907,6 +948,14 @@ export class CodexAcpServer implements acp.Agent {
907948 return toPromptUsage ( lastTokenUsage ) ;
908949 }
909950
951+ private buildCancelledPromptResponse ( sessionState : SessionState ) : acp . PromptResponse {
952+ return {
953+ stopReason : "cancelled" ,
954+ usage : this . buildPromptUsage ( sessionState . lastTokenUsage ) ,
955+ _meta : this . buildQuotaMeta ( sessionState ) ,
956+ } ;
957+ }
958+
910959 private async runWithProcessCheck < T > ( operation : ( ) => Promise < T > ) : Promise < T > {
911960 try {
912961 return await operation ( ) ;
0 commit comments