From 5f1ac6b686904e22e6d65414aa3c0b3b12b54854 Mon Sep 17 00:00:00 2001 From: Sergey Ignatov Date: Thu, 20 Aug 2026 10:32:40 +0200 Subject: [PATCH] fix: suppress late session updates after close --- src/CodexAcpServer.ts | 27 ++++++++++++------- src/CodexCommands.ts | 11 +++++--- .../CodexACPAgent/session-close.test.ts | 15 +++++++++++ 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 6e0c394b..1c06caa9 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -408,7 +408,7 @@ export class CodexAcpServer { } } else if (methodRequest.params.action === "pause") { const goal = await this.runWithProcessCheck(() => this.codexAcpClient.setGoalStatus(sessionState.sessionId, "paused")); - if (this.goalPublishIsCurrent(sessionState, sessionGeneration)) { + if (this.sessionPublishIsCurrent(sessionState, sessionGeneration)) { await this.publishGoalSnapshot(sessionState, toThreadGoalSnapshot(goal), false); } } else if (methodRequest.params.action === "resume") { @@ -420,7 +420,7 @@ export class CodexAcpServer { updatedGoal = goal; }, )); - if (updatedGoal !== null && this.goalPublishIsCurrent(sessionState, sessionGeneration)) { + if (updatedGoal !== null && this.sessionPublishIsCurrent(sessionState, sessionGeneration)) { await this.publishGoalSnapshot(sessionState, toThreadGoalSnapshot(updatedGoal), false); } if (turnCompleted === null && updatedGoal !== null) { @@ -433,7 +433,7 @@ export class CodexAcpServer { } } else if (methodRequest.params.action === "clear") { await this.runWithProcessCheck(() => this.codexAcpClient.clearGoal(sessionState.sessionId)); - if (this.goalPublishIsCurrent(sessionState, sessionGeneration)) { + if (this.sessionPublishIsCurrent(sessionState, sessionGeneration)) { await this.publishGoalSnapshot(sessionState, null, false); } } @@ -638,7 +638,7 @@ export class CodexAcpServer { this.publishMcpStartupStatusAsync(sessionId); } - this.publishAvailableCommandsAsync(sessionState); + this.publishAvailableCommandsAsync(sessionState, sessionGeneration); if ("sessionId" in request) { this.publishCurrentGoalAsync(sessionState, sessionGeneration); } @@ -1310,7 +1310,7 @@ export class CodexAcpServer { sessionId: sessionState.sessionId, prompt: GOAL_CONTINUATION_PROMPT, }, "Goal continuation", async () => { - if (!this.goalPublishIsCurrent(sessionState, sessionGeneration) + if (!this.sessionPublishIsCurrent(sessionState, sessionGeneration) || this.goalControlGenerations.get(sessionState.sessionId) !== goalControlGeneration) { return false; } @@ -1459,8 +1459,15 @@ export class CodexAcpServer { return !isJetBrains2026_1Client(this.clientInfo); } - private publishAvailableCommandsAsync(sessionState: SessionState) { - void this.availableCommands.publish(sessionState); + private publishAvailableCommandsAsync(sessionState: SessionState, sessionGeneration: number): void { + void this.publishAvailableCommands(sessionState, sessionGeneration); + } + + private async publishAvailableCommands(sessionState: SessionState, sessionGeneration: number): Promise { + await this.availableCommands.publish( + sessionState, + () => this.sessionPublishIsCurrent(sessionState, sessionGeneration), + ); } private publishCurrentGoalAsync(sessionState: SessionState, sessionGeneration: number): void { @@ -1487,14 +1494,14 @@ export class CodexAcpServer { const requestRevision = ++sessionState.goalRevision; const goal = await this.runWithProcessCheck(() => this.codexAcpClient.getGoal(sessionState.sessionId)); const snapshot = goal === null ? null : toThreadGoalSnapshot(goal); - if (!this.goalPublishIsCurrent(sessionState, sessionGeneration) + if (!this.sessionPublishIsCurrent(sessionState, sessionGeneration) || sessionState.goalRevision !== requestRevision) { return; } await this.publishGoalSnapshot(sessionState, snapshot, force, false); } - private goalPublishIsCurrent(sessionState: SessionState, sessionGeneration: number): boolean { + private sessionPublishIsCurrent(sessionState: SessionState, sessionGeneration: number): boolean { return this.sessions.get(sessionState.sessionId) === sessionState && this.getSessionGeneration(sessionState.sessionId) === sessionGeneration && !this.sessionIsClosing(sessionState.sessionId); @@ -1636,7 +1643,7 @@ export class CodexAcpServer { this.publishMcpStartupStatusAsync(sessionId); } - await this.availableCommands.publish(sessionState); + await this.publishAvailableCommands(sessionState, requestedSessionGeneration); await this.publishCurrentGoalBestEffort(sessionState, requestedSessionGeneration, true); const sessionModelState: LegacySessionModelState = this.createModelState(models, currentModelId); const sessionModeState: SessionModeState = sessionState.agentMode.toSessionModeState(); diff --git a/src/CodexCommands.ts b/src/CodexCommands.ts index 1d5e86d2..e4cd16d4 100644 --- a/src/CodexCommands.ts +++ b/src/CodexCommands.ts @@ -54,11 +54,14 @@ export class CodexCommands { this.onLogout = onLogout; } - async publish(sessionState: SessionState): Promise { + async publish(sessionState: SessionState, shouldPublish: () => boolean = () => true): Promise { try { + if (!shouldPublish()) { + return; + } const skillsResponse = await this.runWithProcessCheck(() => this.codexAcpClient.listSkills(this.createSkillsListParams(sessionState))); const availableCommands = this.buildAvailableCommands(skillsResponse?.data ?? []); - if (availableCommands.length === 0) { + if (availableCommands.length === 0 || !shouldPublish()) { return; } @@ -68,7 +71,9 @@ export class CodexCommands { availableCommands }); } catch (err) { - logger.error(`Failed to publish available commands for session ${sessionState.sessionId}`, err); + if (shouldPublish()) { + logger.error(`Failed to publish available commands for session ${sessionState.sessionId}`, err); + } } } diff --git a/src/__tests__/CodexACPAgent/session-close.test.ts b/src/__tests__/CodexACPAgent/session-close.test.ts index 97fa73aa..a04b7729 100644 --- a/src/__tests__/CodexACPAgent/session-close.test.ts +++ b/src/__tests__/CodexACPAgent/session-close.test.ts @@ -61,6 +61,21 @@ describe("ACP session close", () => { expect(() => codexAcpAgent.getSessionState(sessionId)).toThrow(`Session ${sessionId} not found`); }); + it("does not publish available commands after close completes", async () => { + const skills = deferred<{data: []}>(); + const {fixture, codexAcpAgent} = await createSession({ + configure: ({codexAcpClient}) => { + vi.spyOn(codexAcpClient, "listSkills").mockReturnValue(skills.promise); + }, + }); + + await codexAcpAgent.closeSession({sessionId}); + skills.resolve({data: []}); + await waitForMicrotasks(); + + expect(fixture.getAcpConnectionEvents([])).toEqual([]); + }); + it("does not wait for delayed turn start before closing", async () => { const {fixture, codexAcpAgent} = await createSession(); const turnStart = deferred();