@@ -13,7 +13,6 @@ import type {
1313 Model ,
1414 ReasoningEffortOption ,
1515 Thread ,
16- ThreadGoalStatus ,
1716 ThreadItem ,
1817 UserInput
1918} from "./app-server/v2" ;
@@ -23,7 +22,6 @@ import {AgentMode, MODE_CONFIG_ID} from "./AgentMode";
2322import {
2423 COLLABORATION_MODE_CONFIG_ID ,
2524 createCollaborationModeConfigOption ,
26- DEFAULT_COLLABORATION_MODE ,
2725 parseCollaborationMode ,
2826} from "./CollaborationModeConfig" ;
2927import type { ModeKind } from "./app-server/ModeKind" ;
@@ -82,14 +80,11 @@ import {
8280 createAgentTextThoughtChunk ,
8381 createUserMessageChunk ,
8482} from "./ContentChunks" ;
85-
86- export interface ThreadGoalSnapshot {
87- objective : string ;
88- status : ThreadGoalStatus ;
89- tokenBudget : number | null ;
90- timeUsedSeconds : number ;
91- controlMethod : typeof GOAL_CONTROL_METHOD ;
92- }
83+ import {
84+ sameThreadGoalSnapshot ,
85+ type ThreadGoalSnapshot ,
86+ toThreadGoalSnapshot ,
87+ } from "./ThreadGoalSnapshot" ;
9388
9489export interface SessionState {
9590 sessionId : string ,
@@ -114,6 +109,7 @@ export interface SessionState {
114109 sessionMcpServers ?: Array < string > ;
115110 terminalOutputMode : TerminalOutputMode ;
116111 currentGoal ?: ThreadGoalSnapshot | null ;
112+ goalRevision : number ;
117113 sessionTitle : string | null ;
118114 sessionTitleSource : "unset" | "fallback" | "explicit" | "unknown" ;
119115}
@@ -263,10 +259,17 @@ export class CodexAcpServer {
263259 if ( ! sessionState ) {
264260 throw RequestError . invalidParams ( undefined , `Unknown session: ${ methodRequest . params . sessionId } ` ) ;
265261 }
262+ const sessionGeneration = this . getSessionGeneration ( sessionState . sessionId ) ;
266263 if ( methodRequest . params . action === "pause" ) {
267- await this . runWithProcessCheck ( ( ) => this . codexAcpClient . setGoalStatus ( sessionState . sessionId , "paused" ) ) ;
268- } else {
264+ const goal = await this . runWithProcessCheck ( ( ) => this . codexAcpClient . setGoalStatus ( sessionState . sessionId , "paused" ) ) ;
265+ if ( this . goalPublishIsCurrent ( sessionState , sessionGeneration ) ) {
266+ await this . publishGoalSnapshot ( sessionState , toThreadGoalSnapshot ( goal ) , false ) ;
267+ }
268+ } else if ( methodRequest . params . action === "clear" ) {
269269 await this . runWithProcessCheck ( ( ) => this . codexAcpClient . clearGoal ( sessionState . sessionId ) ) ;
270+ if ( this . goalPublishIsCurrent ( sessionState , sessionGeneration ) ) {
271+ await this . publishGoalSnapshot ( sessionState , null , false ) ;
272+ }
270273 }
271274 return { } ;
272275 }
@@ -428,7 +431,7 @@ export class CodexAcpServer {
428431 supportedReasoningEfforts : currentModel ?. supportedReasoningEfforts ?? [ ] ,
429432 supportedInputModalities : currentModel ?. inputModalities ?? [ "text" , "image" ] ,
430433 agentMode : AgentMode . getInitialAgentMode ( ) ,
431- collaborationMode : DEFAULT_COLLABORATION_MODE ,
434+ collaborationMode : sessionMetadata . collaborationMode ,
432435 currentTurnId : null ,
433436 lastTokenUsage : null ,
434437 totalTokenUsage : null ,
@@ -443,6 +446,7 @@ export class CodexAcpServer {
443446 currentModelSupportsFast : currentModelSupportsFast ,
444447 sessionMcpServers : sessionMcpServers ,
445448 terminalOutputMode : this . terminalOutputMode ,
449+ goalRevision : 0 ,
446450 sessionTitle : null ,
447451 sessionTitleSource : "sessionId" in request ? "unknown" : "unset" ,
448452 } ;
@@ -459,7 +463,7 @@ export class CodexAcpServer {
459463
460464 this . publishAvailableCommandsAsync ( sessionState ) ;
461465 if ( "sessionId" in request ) {
462- this . publishCurrentGoalAsync ( sessionState ) ;
466+ this . publishCurrentGoalAsync ( sessionState , sessionGeneration ) ;
463467 }
464468 const sessionModelState : LegacySessionModelState = this . createModelState ( models , currentModelId ) ;
465469 const sessionModeState : SessionModeState = sessionState . agentMode . toSessionModeState ( ) ;
@@ -897,21 +901,55 @@ export class CodexAcpServer {
897901 void this . availableCommands . publish ( sessionState ) ;
898902 }
899903
900- private publishCurrentGoalAsync ( sessionState : SessionState ) : void {
901- void this . publishCurrentGoal ( sessionState ) . catch ( ( err ) => {
904+ private publishCurrentGoalAsync ( sessionState : SessionState , sessionGeneration : number ) : void {
905+ void this . publishCurrentGoalBestEffort ( sessionState , sessionGeneration , true ) ;
906+ }
907+
908+ private async publishCurrentGoalBestEffort (
909+ sessionState : SessionState ,
910+ sessionGeneration : number ,
911+ force : boolean ,
912+ ) : Promise < void > {
913+ try {
914+ await this . publishCurrentGoal ( sessionState , sessionGeneration , force ) ;
915+ } catch ( err ) {
902916 logger . error ( `Failed to publish current goal for session ${ sessionState . sessionId } ` , err ) ;
903- } ) ;
917+ }
904918 }
905919
906- private async publishCurrentGoal ( sessionState : SessionState ) : Promise < void > {
920+ private async publishCurrentGoal (
921+ sessionState : SessionState ,
922+ sessionGeneration : number ,
923+ force : boolean ,
924+ ) : Promise < void > {
925+ const requestRevision = ++ sessionState . goalRevision ;
907926 const goal = await this . runWithProcessCheck ( ( ) => this . codexAcpClient . getGoal ( sessionState . sessionId ) ) ;
908- const snapshot : ThreadGoalSnapshot | null = goal === null ? null : {
909- objective : goal . objective . trim ( ) ,
910- status : goal . status ,
911- tokenBudget : goal . tokenBudget ,
912- timeUsedSeconds : goal . timeUsedSeconds ,
913- controlMethod : GOAL_CONTROL_METHOD ,
914- } ;
927+ const snapshot = goal === null ? null : toThreadGoalSnapshot ( goal ) ;
928+ if ( ! this . goalPublishIsCurrent ( sessionState , sessionGeneration )
929+ || sessionState . goalRevision !== requestRevision ) {
930+ return ;
931+ }
932+ await this . publishGoalSnapshot ( sessionState , snapshot , force , false ) ;
933+ }
934+
935+ private goalPublishIsCurrent ( sessionState : SessionState , sessionGeneration : number ) : boolean {
936+ return this . sessions . get ( sessionState . sessionId ) === sessionState
937+ && this . getSessionGeneration ( sessionState . sessionId ) === sessionGeneration
938+ && ! this . sessionIsClosing ( sessionState . sessionId ) ;
939+ }
940+
941+ private async publishGoalSnapshot (
942+ sessionState : SessionState ,
943+ snapshot : ThreadGoalSnapshot | null ,
944+ force : boolean ,
945+ incrementRevision = true ,
946+ ) : Promise < void > {
947+ if ( incrementRevision ) {
948+ sessionState . goalRevision += 1 ;
949+ }
950+ if ( ! force && sameThreadGoalSnapshot ( sessionState . currentGoal , snapshot ) ) {
951+ return ;
952+ }
915953 sessionState . currentGoal = snapshot ;
916954 const session = new ACPSessionConnection ( this . connection , sessionState . sessionId ) ;
917955 await session . update ( {
@@ -1007,7 +1045,7 @@ export class CodexAcpServer {
10071045 supportedReasoningEfforts : currentModel ?. supportedReasoningEfforts ?? [ ] ,
10081046 supportedInputModalities : currentModel ?. inputModalities ?? [ "text" , "image" ] ,
10091047 agentMode : AgentMode . getInitialAgentMode ( ) ,
1010- collaborationMode : DEFAULT_COLLABORATION_MODE ,
1048+ collaborationMode : sessionMetadata . collaborationMode ,
10111049 currentTurnId : null ,
10121050 lastTokenUsage : null ,
10131051 totalTokenUsage : null ,
@@ -1022,6 +1060,7 @@ export class CodexAcpServer {
10221060 currentModelSupportsFast : currentModelSupportsFast ,
10231061 sessionMcpServers : sessionMcpServers ,
10241062 terminalOutputMode : this . terminalOutputMode ,
1063+ goalRevision : 0 ,
10251064 sessionTitle : null ,
10261065 sessionTitleSource : "unset" ,
10271066 } ;
@@ -1037,7 +1076,7 @@ export class CodexAcpServer {
10371076 }
10381077
10391078 await this . availableCommands . publish ( sessionState ) ;
1040- await this . publishCurrentGoal ( sessionState ) ;
1079+ await this . publishCurrentGoalBestEffort ( sessionState , requestedSessionGeneration , true ) ;
10411080 const sessionModelState : LegacySessionModelState = this . createModelState ( models , currentModelId ) ;
10421081 const sessionModeState : SessionModeState = sessionState . agentMode . toSessionModeState ( ) ;
10431082
0 commit comments