From d51550a19c7ceb31d052f8c0afd70ce8beb85ee2 Mon Sep 17 00:00:00 2001 From: "Nikolai.Sviridov" Date: Thu, 7 May 2026 10:49:55 +0200 Subject: [PATCH 1/2] feat: LLM-27711 Codex /compact command --- src/CodexAcpClient.ts | 6 + src/CodexAcpServer.ts | 74 ++++++--- src/CodexAppServerClient.ts | 6 + src/CodexCommands.ts | 13 +- src/CodexEventHandler.ts | 16 +- .../CodexACPAgent/CodexAcpClient.test.ts | 157 +++++++++++++++++- .../data/available-commands-build-in.json | 5 + .../data/available-commands-skills.json | 5 + .../data/context-compaction-progress.json | 43 +++++ .../data/load-session-history.json | 5 + src/__tests__/acp-test-utils.ts | 19 ++- 11 files changed, 316 insertions(+), 33 deletions(-) create mode 100644 src/__tests__/CodexACPAgent/data/context-compaction-progress.json diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index d5b59f35..1cf36f17 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -415,6 +415,12 @@ export class CodexAcpClient { }); } + async compactSession(sessionId: string): Promise { + const turnCompleted = this.codexClient.awaitTurnCompleted(sessionId); + await this.codexClient.threadCompactStart({ threadId: sessionId }); + return await turnCompleted; + } + async listSkills(params?: SkillsListParams): Promise { return this.codexClient.listSkills(params ?? {}); } diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 1a80d6ca..ff317cbe 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -15,6 +15,7 @@ import type { ReasoningEffortOption, Thread, ThreadItem, + TurnCompletedNotification, UserInput } from "./app-server/v2"; import type {RateLimitsMap} from "./RateLimitsMap"; @@ -742,13 +743,20 @@ export class CodexAcpServer implements acp.Agent { approvalHandler, elicitationHandler); - if (await this.availableCommands.tryHandle(params.prompt, sessionState)) { + const commandResult = await this.availableCommands.tryHandle(params.prompt, sessionState); + if (commandResult) { logger.log("Prompt handled by a command"); - return { - stopReason: "end_turn", - usage: this.buildPromptUsage(sessionState.lastTokenUsage), - _meta: this.buildQuotaMeta(sessionState), - }; + if (commandResult !== true) { + const interruptedResponse = await this.createInterruptedResponseIfNeeded(params.sessionId, commandResult, sessionState); + if (interruptedResponse) { + return interruptedResponse; + } + } + const error = eventHandler.getFailure(); + if (error) { + throw error; + } + return this.createPromptResponse("end_turn", sessionState); } const modelId = ModelId.fromString(sessionState.currentModelId); @@ -771,22 +779,9 @@ export class CodexAcpServer implements acp.Agent { () => this.codexAcpClient.sendPrompt(params, agentMode, modelId, disableSummary, sessionState.cwd)); // Check if turn was interrupted (cancelled) - if (turnCompleted.turn.status === "interrupted") { - await this.connection.sessionUpdate({ - sessionId: params.sessionId, - update: { - sessionUpdate: "agent_message_chunk", - content: { - type: "text", - text: "*Conversation interrupted*" - } - } - }); - return { - stopReason: "cancelled", - usage: this.buildPromptUsage(sessionState.lastTokenUsage), - _meta: this.buildQuotaMeta(sessionState), - }; + const interruptedResponse = await this.createInterruptedResponseIfNeeded(params.sessionId, turnCompleted, sessionState); + if (interruptedResponse) { + return interruptedResponse; } const error = eventHandler.getFailure() @@ -795,11 +790,7 @@ export class CodexAcpServer implements acp.Agent { throw error; } - return { - stopReason: "end_turn", - usage: this.buildPromptUsage(sessionState.lastTokenUsage), - _meta: this.buildQuotaMeta(sessionState), - }; + return this.createPromptResponse("end_turn", sessionState); } catch (err) { logger.error(`Prompt for session ${params.sessionId} failed`, err); throw err; @@ -835,6 +826,35 @@ export class CodexAcpServer implements acp.Agent { return toPromptUsage(lastTokenUsage); } + private async createInterruptedResponseIfNeeded( + sessionId: string, + turnCompleted: TurnCompletedNotification, + sessionState: SessionState + ): Promise { + if (turnCompleted.turn.status !== "interrupted") { + return null; + } + await this.connection.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_message_chunk", + content: { + type: "text", + text: "*Conversation interrupted*" + } + } + }); + return this.createPromptResponse("cancelled", sessionState); + } + + private createPromptResponse(stopReason: acp.PromptResponse["stopReason"], sessionState: SessionState): acp.PromptResponse { + return { + stopReason, + usage: this.buildPromptUsage(sessionState.lastTokenUsage), + _meta: this.buildQuotaMeta(sessionState), + }; + } + private async runWithProcessCheck(operation: () => Promise): Promise { try { return await operation(); diff --git a/src/CodexAppServerClient.ts b/src/CodexAppServerClient.ts index 26b20627..d7c92551 100644 --- a/src/CodexAppServerClient.ts +++ b/src/CodexAppServerClient.ts @@ -26,6 +26,8 @@ import type { SkillsListResponse, ThreadLoadedListParams, ThreadLoadedListResponse, + ThreadCompactStartParams, + ThreadCompactStartResponse, ThreadListParams, ThreadListResponse, ThreadReadParams, @@ -205,6 +207,10 @@ export class CodexAppServerClient { return await this.sendRequest({ method: "thread/read", params: params }); } + async threadCompactStart(params: ThreadCompactStartParams): Promise { + return await this.sendRequest({ method: "thread/compact/start", params }); + } + async listMcpServerStatus(params: ListMcpServerStatusParams): Promise { return await this.sendRequest({ method: "mcpServerStatus/list", params }); } diff --git a/src/CodexCommands.ts b/src/CodexCommands.ts index c81e6429..6413c055 100644 --- a/src/CodexCommands.ts +++ b/src/CodexCommands.ts @@ -3,6 +3,7 @@ import type {AgentSideConnection, AvailableCommand} from "@agentclientprotocol/s import {ACPSessionConnection} from "./ACPSessionConnection"; import type {CodexAcpClient} from "./CodexAcpClient"; import type {RateLimitSnapshot, SkillsListEntry} from "./app-server/v2"; +import type {TurnCompletedNotification} from "./app-server/v2"; import type {SessionState} from "./CodexAcpServer"; import type {RateLimitsMap} from "./RateLimitsMap"; import type {TokenCount} from "./TokenCount"; @@ -41,7 +42,7 @@ export class CodexCommands { } } - async tryHandle(prompt: acp.ContentBlock[], sessionState: SessionState): Promise { + async tryHandle(prompt: acp.ContentBlock[], sessionState: SessionState): Promise { const command = this.parseCommand(prompt); if (command) { return this.handleCommand(command, sessionState); @@ -91,6 +92,11 @@ export class CodexCommands { description: "Display session configuration and token usage.", input: null }, + { + name: "compact", + description: "Compact conversation history to reduce context usage.", + input: null + }, { name: "logout", description: "Sign out of Codex. This option is available when you are logged in via ChatGPT.", @@ -119,10 +125,12 @@ export class CodexCommands { }; } - async handleCommand(command: ParsedCommand, sessionState: SessionState): Promise { + async handleCommand(command: ParsedCommand, sessionState: SessionState): Promise { const sessionId = sessionState.sessionId; switch (command.name) { + case "compact": + return await this.runWithProcessCheck(() => this.codexAcpClient.compactSession(sessionId)); case "status": { const session = new ACPSessionConnection(this.connection, sessionId); const message = this.buildStatusMessage(sessionState); @@ -355,3 +363,4 @@ export class CodexCommands { } type ParsedCommand = { name: string; input: string | null }; +type CommandHandlingResult = true | TurnCompletedNotification; diff --git a/src/CodexEventHandler.ts b/src/CodexEventHandler.ts index d752cfc0..ef5ccf12 100644 --- a/src/CodexEventHandler.ts +++ b/src/CodexEventHandler.ts @@ -227,6 +227,14 @@ export class CodexEventHandler { return await createMcpToolCallUpdate(event.item); case "dynamicToolCall": return await createDynamicToolCallUpdate(event.item); + case "contextCompaction": + return { + sessionUpdate: "tool_call", + toolCallId: event.item.id, + kind: "other", + title: "Compacting context", + status: "in_progress", + }; case "collabAgentToolCall": case "userMessage": case "hookPrompt": @@ -237,7 +245,6 @@ export class CodexEventHandler { case "imageGeneration": case "enteredReviewMode": case "exitedReviewMode": - case "contextCompaction": case "plan": return null; } @@ -272,6 +279,12 @@ export class CodexEventHandler { text: summary } } + case "contextCompaction": + return { + sessionUpdate: "tool_call_update", + toolCallId: event.item.id, + status: "completed", + }; case "collabAgentToolCall": case "userMessage": case "hookPrompt": @@ -281,7 +294,6 @@ export class CodexEventHandler { case "imageGeneration": case "enteredReviewMode": case "exitedReviewMode": - case "contextCompaction": case "plan": return null; } diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 27ff179f..917aab25 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -7,10 +7,25 @@ import {createTestFixture, createCodexMockTestFixture, createTestSessionState, t import type {ServerNotification} from "../../app-server"; import type {SessionState} from "../../CodexAcpServer"; import {AgentMode} from "../../AgentMode"; -import type {ListMcpServerStatusResponse, Model, SkillsListResponse, TurnStartParams} from "../../app-server/v2"; +import type {ListMcpServerStatusResponse, Model, SkillsListResponse, TurnCompletedNotification, TurnStatus, TurnStartParams} from "../../app-server/v2"; import type {RateLimitsMap} from "../../RateLimitsMap"; import {ModelId} from "../../ModelId"; +function createTurnCompletedNotification(threadId: string, status: TurnStatus): TurnCompletedNotification { + return { + threadId, + turn: { + id: "turn-id", + items: [], + status, + error: null, + startedAt: null, + completedAt: null, + durationMs: null, + } + }; +} + describe('ACP server test', { timeout: 40_000 }, () => { let fixture: TestFixture; @@ -735,6 +750,101 @@ describe('ACP server test', { timeout: 40_000 }, () => { await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot("data/command-status.json"); }); + it('handles compact command via thread/compact/start', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAcpAgent = mockFixture.getCodexAcpAgent(); + const sessionState: SessionState = createTestSessionState({ sessionId: "session-id" }); + vi.spyOn(codexAcpAgent, "getSessionState").mockReturnValue(sessionState); + + const promptPromise = codexAcpAgent.prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/compact" }] + }); + + await vi.waitFor(() => { + expect(mockFixture.getCodexConnectionEvents([]).some(event => + event.eventType === "request" + && event.method === "thread/compact/start" + && event.params.threadId === "session-id" + )).toBe(true); + }); + + mockFixture.sendServerNotification({ + method: "turn/completed", + params: createTurnCompletedNotification("session-id", "completed") + }); + + await expect(promptPromise).resolves.toEqual(expect.objectContaining({ stopReason: "end_turn" })); + expect(mockFixture.getCodexConnectionEvents([])).toContainEqual(expect.objectContaining({ + eventType: "request", + method: "thread/compact/start", + params: { threadId: "session-id" }, + })); + }); + + it('waits for compact command turn/completed from the same thread', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAcpAgent = mockFixture.getCodexAcpAgent(); + const sessionState: SessionState = createTestSessionState({ sessionId: "session-id" }); + vi.spyOn(codexAcpAgent, "getSessionState").mockReturnValue(sessionState); + + let resolved = false; + const promptPromise = codexAcpAgent.prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/compact" }] + }).then(response => { + resolved = true; + return response; + }); + + await vi.waitFor(() => { + expect(mockFixture.getCodexConnectionEvents([]).some(event => + event.eventType === "request" && event.method === "thread/compact/start" + )).toBe(true); + }); + + mockFixture.sendServerNotification({ + method: "turn/completed", + params: createTurnCompletedNotification("other-session-id", "completed") + }); + + await new Promise(resolve => setTimeout(resolve, 0)); + expect(resolved).toBe(false); + + mockFixture.sendServerNotification({ + method: "turn/completed", + params: createTurnCompletedNotification("session-id", "completed") + }); + + await expect(promptPromise).resolves.toEqual(expect.objectContaining({ stopReason: "end_turn" })); + expect(resolved).toBe(true); + }); + + it('maps interrupted compact command to cancelled prompt response', async () => { + const mockFixture = createCodexMockTestFixture(); + const codexAcpAgent = mockFixture.getCodexAcpAgent(); + const sessionState: SessionState = createTestSessionState({ sessionId: "session-id" }); + vi.spyOn(codexAcpAgent, "getSessionState").mockReturnValue(sessionState); + + const promptPromise = codexAcpAgent.prompt({ + sessionId: "session-id", + prompt: [{ type: "text", text: "/compact" }] + }); + + await vi.waitFor(() => { + expect(mockFixture.getCodexConnectionEvents([]).some(event => + event.eventType === "request" && event.method === "thread/compact/start" + )).toBe(true); + }); + + mockFixture.sendServerNotification({ + method: "turn/completed", + params: createTurnCompletedNotification("session-id", "interrupted") + }); + + await expect(promptPromise).resolves.toEqual(expect.objectContaining({ stopReason: "cancelled" })); + }); + it('handles logout command', async () => { const mockFixture = createCodexMockTestFixture(); const codexAcpAgent = mockFixture.getCodexAcpAgent(); @@ -1032,6 +1142,51 @@ describe('ACP server test', { timeout: 40_000 }, () => { await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot("data/thread-compacted.json"); }); + it ('should surface context compaction progress and final message', async () => { + const sessionId = "test-session-id"; + const { mockFixture } = setupPromptFixture({ sessionId }); + + await mockFixture.getCodexAcpAgent().prompt({ + sessionId, + prompt: [{ type: "text", text: "test" }], + }); + + mockFixture.clearAcpConnectionDump(); + + mockFixture.sendServerNotification({ + method: "item/started", + params: { + threadId: sessionId, + turnId: "turn-id", + item: { type: "contextCompaction", id: "compact-item-id" } + } + }); + await vi.waitFor(() => { + expect(mockFixture.getAcpConnectionEvents([])).toHaveLength(1); + }); + mockFixture.sendServerNotification({ + method: "item/completed", + params: { + threadId: sessionId, + turnId: "turn-id", + item: { type: "contextCompaction", id: "compact-item-id" } + } + }); + await vi.waitFor(() => { + expect(mockFixture.getAcpConnectionEvents([])).toHaveLength(2); + }); + mockFixture.sendServerNotification({ + method: "thread/compacted", + params: { threadId: sessionId, turnId: "turn-id" } + }); + + await vi.waitFor(() => { + expect(mockFixture.getAcpConnectionEvents([])).toHaveLength(3); + }); + + await expect(mockFixture.getAcpConnectionDump([])).toMatchFileSnapshot("data/context-compaction-progress.json"); + }); + it ('should accumulate rate limits from multiple notifications', async () => { const sessionId = "test-session-id"; const { mockFixture, sessionState } = setupPromptFixture({ sessionId }); diff --git a/src/__tests__/CodexACPAgent/data/available-commands-build-in.json b/src/__tests__/CodexACPAgent/data/available-commands-build-in.json index 42999913..26495c52 100644 --- a/src/__tests__/CodexACPAgent/data/available-commands-build-in.json +++ b/src/__tests__/CodexACPAgent/data/available-commands-build-in.json @@ -21,6 +21,11 @@ "description": "Display session configuration and token usage.", "input": null }, + { + "name": "compact", + "description": "Compact conversation history to reduce context usage.", + "input": null + }, { "name": "logout", "description": "Sign out of Codex. This option is available when you are logged in via ChatGPT.", diff --git a/src/__tests__/CodexACPAgent/data/available-commands-skills.json b/src/__tests__/CodexACPAgent/data/available-commands-skills.json index cc64ab60..34a18c6f 100644 --- a/src/__tests__/CodexACPAgent/data/available-commands-skills.json +++ b/src/__tests__/CodexACPAgent/data/available-commands-skills.json @@ -21,6 +21,11 @@ "description": "Display session configuration and token usage.", "input": null }, + { + "name": "compact", + "description": "Compact conversation history to reduce context usage.", + "input": null + }, { "name": "logout", "description": "Sign out of Codex. This option is available when you are logged in via ChatGPT.", diff --git a/src/__tests__/CodexACPAgent/data/context-compaction-progress.json b/src/__tests__/CodexACPAgent/data/context-compaction-progress.json new file mode 100644 index 00000000..7c13c958 --- /dev/null +++ b/src/__tests__/CodexACPAgent/data/context-compaction-progress.json @@ -0,0 +1,43 @@ +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "tool_call", + "toolCallId": "compact-item-id", + "kind": "other", + "title": "Compacting context", + "status": "in_progress" + } + } + ] +} +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "tool_call_update", + "toolCallId": "compact-item-id", + "status": "completed" + } + } + ] +} +{ + "method": "sessionUpdate", + "args": [ + { + "sessionId": "test-session-id", + "update": { + "sessionUpdate": "agent_message_chunk", + "content": { + "type": "text", + "text": "*Context compacted to fit the model's context window.*\n\n" + } + } + } + ] +} \ No newline at end of file diff --git a/src/__tests__/CodexACPAgent/data/load-session-history.json b/src/__tests__/CodexACPAgent/data/load-session-history.json index 8e962238..6d03fc57 100644 --- a/src/__tests__/CodexACPAgent/data/load-session-history.json +++ b/src/__tests__/CodexACPAgent/data/load-session-history.json @@ -21,6 +21,11 @@ "description": "Display session configuration and token usage.", "input": null }, + { + "name": "compact", + "description": "Compact conversation history to reduce context usage.", + "input": null + }, { "name": "logout", "description": "Sign out of Codex. This option is available when you are logged in via ChatGPT.", diff --git a/src/__tests__/acp-test-utils.ts b/src/__tests__/acp-test-utils.ts index a1629214..77d1cfc7 100644 --- a/src/__tests__/acp-test-utils.ts +++ b/src/__tests__/acp-test-utils.ts @@ -216,6 +216,7 @@ export interface CodexMockTestFixture extends TestFixture { export function createCodexMockTestFixture(): CodexMockTestFixture { let unhandledNotificationHandler: ((notification: any) => void) | null = null; const requestHandlers = new Map Promise>(); + const notificationHandlers = new Map void>>(); // State for controlling permission responses const permissionState: { response: RequestPermissionResponse } = { @@ -227,7 +228,17 @@ export function createCodexMockTestFixture(): CodexMockTestFixture { onUnhandledNotification: (handler: (notification: any) => void) => { unhandledNotificationHandler = handler; }, - onNotification: () => {}, + onNotification: (method: string, handler: (params: unknown) => void) => { + const handlers = notificationHandlers.get(method) ?? []; + handlers.push(handler); + notificationHandlers.set(method, handlers); + return { + dispose: () => { + const currentHandlers = notificationHandlers.get(method) ?? []; + notificationHandlers.set(method, currentHandlers.filter(current => current !== handler)); + } + }; + }, onRequest: (type: { method: string }, handler: (params: unknown) => Promise) => { requestHandlers.set(type.method, handler); }, @@ -258,6 +269,12 @@ export function createCodexMockTestFixture(): CodexMockTestFixture { return { ...baseFixture, sendServerNotification(notification: ServerNotification | Record): void { + const method = typeof notification.method === "string" ? notification.method : null; + if (method) { + for (const handler of notificationHandlers.get(method) ?? []) { + handler(notification.params); + } + } if (unhandledNotificationHandler) { unhandledNotificationHandler(notification); } From e25c5715bfc17a7e4e2cd56cbc632313c00c0356 Mon Sep 17 00:00:00 2001 From: "Nikolai.Sviridov" Date: Tue, 12 May 2026 16:53:24 +0200 Subject: [PATCH 2/2] fix: tests --- src/CodexAcpClient.ts | 4 +- src/CodexAppServerClient.ts | 15 +++++++ .../CodexACPAgent/CodexAcpClient.test.ts | 42 ++++--------------- 3 files changed, 23 insertions(+), 38 deletions(-) diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 1cf36f17..12d0e878 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -416,9 +416,7 @@ export class CodexAcpClient { } async compactSession(sessionId: string): Promise { - const turnCompleted = this.codexClient.awaitTurnCompleted(sessionId); - await this.codexClient.threadCompactStart({ threadId: sessionId }); - return await turnCompleted; + return await this.codexClient.runCompact({ threadId: sessionId }); } async listSkills(params?: SkillsListParams): Promise { diff --git a/src/CodexAppServerClient.ts b/src/CodexAppServerClient.ts index d7c92551..655e64be 100644 --- a/src/CodexAppServerClient.ts +++ b/src/CodexAppServerClient.ts @@ -183,6 +183,21 @@ export class CodexAppServerClient { } } + async runCompact(params: ThreadCompactStartParams): Promise { + let resolveTurnCompleted!: (event: TurnCompletedNotification) => void; + const turnCompleted = new Promise((resolve) => { + resolveTurnCompleted = resolve; + }); + const releaseCapture = this.captureTurnCompletions(params.threadId, resolveTurnCompleted); + + try { + await this.threadCompactStart(params); + return await turnCompleted; + } finally { + releaseCapture(); + } + } + async turnInterrupt(params: TurnInterruptParams): Promise { return await this.sendRequest({ method: "turn/interrupt", params: params }); } diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 917aab25..029ac894 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -10,22 +10,6 @@ import {AgentMode} from "../../AgentMode"; import type {ListMcpServerStatusResponse, Model, SkillsListResponse, TurnCompletedNotification, TurnStatus, TurnStartParams} from "../../app-server/v2"; import type {RateLimitsMap} from "../../RateLimitsMap"; import {ModelId} from "../../ModelId"; - -function createTurnCompletedNotification(threadId: string, status: TurnStatus): TurnCompletedNotification { - return { - threadId, - turn: { - id: "turn-id", - items: [], - status, - error: null, - startedAt: null, - completedAt: null, - durationMs: null, - } - }; -} - describe('ACP server test', { timeout: 40_000 }, () => { let fixture: TestFixture; @@ -422,7 +406,7 @@ describe('ACP server test', { timeout: 40_000 }, () => { return onServerNotification; } - function createTurn(id: string, status: "inProgress" | "completed") { + function createTurn(id: string, status: TurnStatus) { return { id, items: [], @@ -434,12 +418,12 @@ describe('ACP server test', { timeout: 40_000 }, () => { }; } - function createTurnCompletedNotification(threadId: string, turnId: string): ServerNotification { + function createTurnCompletedNotification(threadId: string, turnId: string, status: TurnStatus = "completed"): ServerNotification { return { method: "turn/completed", params: { threadId, - turn: createTurn(turnId, "completed"), + turn: createTurn(turnId, status), }, }; } @@ -769,10 +753,7 @@ describe('ACP server test', { timeout: 40_000 }, () => { )).toBe(true); }); - mockFixture.sendServerNotification({ - method: "turn/completed", - params: createTurnCompletedNotification("session-id", "completed") - }); + mockFixture.sendServerNotification(createTurnCompletedNotification("session-id", "compact-turn-id")); await expect(promptPromise).resolves.toEqual(expect.objectContaining({ stopReason: "end_turn" })); expect(mockFixture.getCodexConnectionEvents([])).toContainEqual(expect.objectContaining({ @@ -803,18 +784,12 @@ describe('ACP server test', { timeout: 40_000 }, () => { )).toBe(true); }); - mockFixture.sendServerNotification({ - method: "turn/completed", - params: createTurnCompletedNotification("other-session-id", "completed") - }); + mockFixture.sendServerNotification(createTurnCompletedNotification("other-session-id", "other-turn-id")); await new Promise(resolve => setTimeout(resolve, 0)); expect(resolved).toBe(false); - mockFixture.sendServerNotification({ - method: "turn/completed", - params: createTurnCompletedNotification("session-id", "completed") - }); + mockFixture.sendServerNotification(createTurnCompletedNotification("session-id", "compact-turn-id")); await expect(promptPromise).resolves.toEqual(expect.objectContaining({ stopReason: "end_turn" })); expect(resolved).toBe(true); @@ -837,10 +812,7 @@ describe('ACP server test', { timeout: 40_000 }, () => { )).toBe(true); }); - mockFixture.sendServerNotification({ - method: "turn/completed", - params: createTurnCompletedNotification("session-id", "interrupted") - }); + mockFixture.sendServerNotification(createTurnCompletedNotification("session-id", "compact-turn-id", "interrupted")); await expect(promptPromise).resolves.toEqual(expect.objectContaining({ stopReason: "cancelled" })); });