@@ -35,6 +35,7 @@ import {
3535import type { TokenCount } from "./TokenCount" ;
3636import { toPromptUsage } from "./TokenCount" ;
3737import { CodexCommands } from "./CodexCommands" ;
38+ import { SteeringQueue } from "./SteeringQueue" ;
3839import type { QuotaMeta } from "./QuotaMeta" ;
3940import { logger } from "./Logger" ;
4041import { sanitizeMcpServerName } from "./McpServerName" ;
@@ -166,7 +167,7 @@ export class CodexAcpServer {
166167 private readonly pendingMcpStartupSessions : Map < string , PendingMcpStartupSession > ;
167168 private readonly pendingTurnStarts : Map < string , PendingTurnStart > ;
168169 private readonly activePrompts : Map < string , ActivePrompt > ;
169- private readonly steeringRequests : Map < string , Promise < void > > ;
170+ private readonly steeringQueues : Map < string , SteeringQueue > ;
170171 private readonly closingSessions : Map < string , number > ;
171172 private readonly sessionGenerations : Map < string , number > ;
172173 private readonly sessionOpenGenerations : Map < string , number > ;
@@ -182,7 +183,7 @@ export class CodexAcpServer {
182183 this . pendingMcpStartupSessions = new Map ( ) ;
183184 this . pendingTurnStarts = new Map ( ) ;
184185 this . activePrompts = new Map ( ) ;
185- this . steeringRequests = new Map ( ) ;
186+ this . steeringQueues = new Map ( ) ;
186187 this . closingSessions = new Map ( ) ;
187188 this . sessionGenerations = new Map ( ) ;
188189 this . sessionOpenGenerations = new Map ( ) ;
@@ -266,7 +267,7 @@ export class CodexAcpServer {
266267 case LEGACY_SET_SESSION_MODEL_METHOD :
267268 return await this . unstable_setSessionModel ( this . parseLegacySetSessionModelParams ( methodRequest . params ) ) ;
268269 case SESSION_STEERING_METHOD :
269- return await this . steerSessionWithFallback ( this . parseSessionSteerParams ( methodRequest . params ) ) ;
270+ return await this . executeOrQueueSteeringRequest ( this . parseSessionSteerParams ( methodRequest . params ) ) ;
270271 case GOAL_CONTROL_METHOD : {
271272 const sessionState = this . sessions . get ( methodRequest . params . sessionId ) ;
272273 if ( ! sessionState ) {
@@ -601,6 +602,7 @@ export class CodexAcpServer {
601602 this . pendingMcpStartupSessions . delete ( params . sessionId ) ;
602603 this . pendingTurnStarts . delete ( params . sessionId ) ;
603604 this . activePrompts . delete ( params . sessionId ) ;
605+ this . steeringQueues . delete ( params . sessionId ) ;
604606 }
605607 this . endSessionCloseFence ( params . sessionId ) ;
606608 }
@@ -873,26 +875,49 @@ export class CodexAcpServer {
873875 } ;
874876 }
875877
876- async steerSessionWithFallback ( params : SessionSteerRequest ) : Promise < SessionSteeringResponse > {
877- const previousRequest = this . steeringRequests . get ( params . sessionId ) ?? Promise . resolve ( ) ;
878- let releaseRequest : ( ) => void = ( ) => { } ;
879- const requestCompleted = new Promise < void > ( ( resolve ) => {
880- releaseRequest = resolve ;
881- } ) ;
882- const requestQueue = previousRequest . then ( ( ) => requestCompleted ) ;
883- this . steeringRequests . set ( params . sessionId , requestQueue ) ;
884-
885- await previousRequest ;
878+ /**
879+ * Handles one incoming steering request, serialising it against any other
880+ * steer already in flight for the same session.
881+ *
882+ * Every session gets its own {@link SteeringQueue}: the request is enqueued
883+ * and awaited, so concurrent steers for one session run strictly one at a
884+ * time, in arrival order, and can never race to inject into — or start —
885+ * rival turns. Steers for different sessions use different queues and run
886+ * concurrently. Once the queue drains to idle it is removed from the map,
887+ * so no per-session entry leaks after the session goes quiet (the identity
888+ * check guards against deleting a queue a later request has since reused).
889+ *
890+ * @param params The target session id and the prompt to steer with.
891+ * @returns Whether the prompt joined the active turn ("injected") or started
892+ * a new one ("startedNewTurn"); see {@link performSteeringRequest}.
893+ */
894+ async executeOrQueueSteeringRequest ( params : SessionSteerRequest ) : Promise < SessionSteeringResponse > {
895+ const queue = this . getSteeringQueue ( params . sessionId ) ;
886896 try {
887- return await this . performSteeringRequest ( params ) ;
897+ return await queue . enqueue ( params ) ;
888898 } finally {
889- releaseRequest ( ) ;
890- if ( this . steeringRequests . get ( params . sessionId ) === requestQueue ) {
891- this . steeringRequests . delete ( params . sessionId ) ;
899+ if ( queue . isIdle && this . steeringQueues . get ( params . sessionId ) === queue ) {
900+ this . steeringQueues . delete ( params . sessionId ) ;
892901 }
893902 }
894903 }
895904
905+ /**
906+ * Returns the steering queue for a session, creating and registering it on
907+ * first use.
908+ *
909+ * @param sessionId The session whose steering queue is required.
910+ * @returns The session's existing queue, or a freshly created one.
911+ */
912+ private getSteeringQueue ( sessionId : string ) : SteeringQueue {
913+ let queue = this . steeringQueues . get ( sessionId ) ;
914+ if ( ! queue ) {
915+ queue = new SteeringQueue ( ( params ) => this . performSteeringRequest ( params ) ) ;
916+ this . steeringQueues . set ( sessionId , queue ) ;
917+ }
918+ return queue ;
919+ }
920+
896921 private async performSteeringRequest ( params : SessionSteerRequest ) : Promise < SessionSteeringResponse > {
897922 logger . log ( "Steering session requested" , {
898923 sessionId : params . sessionId ,
0 commit comments