Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 17 additions & 10 deletions src/CodexAcpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Expand All @@ -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) {
Expand All @@ -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);
}
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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<void> {
await this.availableCommands.publish(
sessionState,
() => this.sessionPublishIsCurrent(sessionState, sessionGeneration),
);
}

private publishCurrentGoalAsync(sessionState: SessionState, sessionGeneration: number): void {
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
11 changes: 8 additions & 3 deletions src/CodexCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,14 @@ export class CodexCommands {
this.onLogout = onLogout;
}

async publish(sessionState: SessionState): Promise<void> {
async publish(sessionState: SessionState, shouldPublish: () => boolean = () => true): Promise<void> {
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;
}

Expand All @@ -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);
}
}
}

Expand Down
15 changes: 15 additions & 0 deletions src/__tests__/CodexACPAgent/session-close.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<TurnStartResponse>();
Expand Down