Skip to content

Commit 5f8491a

Browse files
committed
Interrupt late-started goal commands on cancel
1 parent 5edcfe0 commit 5f8491a

2 files changed

Lines changed: 67 additions & 9 deletions

File tree

src/CodexCommands.ts

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ export class CodexCommands {
165165
return { handled: true };
166166
}
167167
case "goal": {
168-
return await this.runGoalCommand(sessionState, command.rest);
168+
return await this.runGoalCommand(sessionState, command.rest, options);
169169
}
170170
case "review": {
171171
const target = this.buildReviewTarget(command.rest);
@@ -266,16 +266,16 @@ export class CodexCommands {
266266
sessionState.sessionId,
267267
target,
268268
(turnId, threadId) => {
269-
if (options.onTurnStarted) {
270-
options.onTurnStarted(turnId, threadId);
271-
} else {
272-
sessionState.currentTurnId = turnId;
273-
}
269+
this.handleCommandTurnStarted(sessionState, options, turnId, threadId);
274270
},
275271
));
276272
}
277273

278-
private async runGoalCommand(sessionState: SessionState, rest: string): Promise<CommandHandleResult> {
274+
private async runGoalCommand(
275+
sessionState: SessionState,
276+
rest: string,
277+
options: CommandHandleOptions,
278+
): Promise<CommandHandleResult> {
279279
const sessionId = sessionState.sessionId;
280280
const argument = rest.trim();
281281
if (argument.length === 0) {
@@ -291,7 +291,7 @@ export class CodexCommands {
291291
return this.createGoalCommandResult(await this.runWithProcessCheck(() => this.codexAcpClient.resumeGoal(
292292
sessionId,
293293
(turnId) => {
294-
sessionState.currentTurnId = turnId;
294+
this.handleCommandTurnStarted(sessionState, options, turnId, sessionId);
295295
},
296296
)));
297297
case "clear":
@@ -315,11 +315,24 @@ export class CodexCommands {
315315
sessionId,
316316
argument,
317317
(turnId) => {
318-
sessionState.currentTurnId = turnId;
318+
this.handleCommandTurnStarted(sessionState, options, turnId, sessionId);
319319
},
320320
)));
321321
}
322322

323+
private handleCommandTurnStarted(
324+
sessionState: SessionState,
325+
options: CommandHandleOptions,
326+
turnId: string,
327+
threadId: string,
328+
): void {
329+
if (options.onTurnStarted) {
330+
options.onTurnStarted(turnId, threadId);
331+
} else {
332+
sessionState.currentTurnId = turnId;
333+
}
334+
}
335+
323336
private createGoalCommandResult(turnCompleted: TurnCompletedNotification | null): CommandHandleResult {
324337
if (turnCompleted === null) {
325338
return { handled: true };

src/__tests__/CodexACPAgent/CodexAcpClient.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1491,6 +1491,51 @@ describe('ACP server test', { timeout: 40_000 }, () => {
14911491
expect(promptResolved).toBe(true);
14921492
});
14931493

1494+
it('interrupts a late-started goal slash command after the ACP prompt request is cancelled', async () => {
1495+
const { mockFixture } = setupPromptFixture();
1496+
const goalCompleted = deferred<TurnCompletedNotification | null>();
1497+
let startGoalTurn: () => void = () => {};
1498+
const goalRunSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "runGoalSet")
1499+
.mockImplementation((_params, onTurnStarted) => {
1500+
startGoalTurn = () => onTurnStarted?.("goal-turn-id");
1501+
return goalCompleted.promise;
1502+
});
1503+
const turnInterruptSpy = vi.spyOn(mockFixture.getCodexAcpClient(), "turnInterrupt")
1504+
.mockImplementation(async ({threadId, turnId}) => {
1505+
goalCompleted.resolve({
1506+
threadId,
1507+
turn: createTurn(turnId, "interrupted"),
1508+
});
1509+
});
1510+
const controller = new AbortController();
1511+
1512+
const promptPromise = mockFixture.getCodexAcpAgent().prompt({
1513+
sessionId: "session-id",
1514+
prompt: [{ type: "text", text: "/goal Ship the migration and keep tests green" }],
1515+
}, controller.signal);
1516+
1517+
await vi.waitFor(() => {
1518+
expect(goalRunSpy).toHaveBeenCalledWith({
1519+
threadId: "session-id",
1520+
objective: "Ship the migration and keep tests green",
1521+
status: "active",
1522+
}, expect.any(Function));
1523+
});
1524+
1525+
controller.abort();
1526+
await expect(promptPromise).resolves.toMatchObject({stopReason: "cancelled"});
1527+
expect(turnInterruptSpy).not.toHaveBeenCalled();
1528+
1529+
startGoalTurn();
1530+
1531+
await vi.waitFor(() => {
1532+
expect(turnInterruptSpy).toHaveBeenCalledWith({
1533+
threadId: "session-id",
1534+
turnId: "goal-turn-id",
1535+
});
1536+
});
1537+
});
1538+
14941539
it('does not hang when goal set starts no continuation turn', async () => {
14951540
const mockFixture = createCodexMockTestFixture();
14961541
const codexAppServerClient = mockFixture.getCodexAppServerClient();

0 commit comments

Comments
 (0)