From 976c24c4c06658cf3cfad7e27464f8e7c7ff6851 Mon Sep 17 00:00:00 2001 From: Serhii Vecherenko Date: Sun, 2 Aug 2026 22:41:16 -0700 Subject: [PATCH] fix(codex): clear existing goal before /goal replacement When Codex /goal replaces an active goal, clear it via thread/goal/clear before setting the new objective to avoid stale goal state, and set the new goal directly in controlGoal edit path instead of dispatching. - clear stale goal before thread/goal/set on replacement - bypass dispatch for controlGoal edit and set status active - add test covering goal replacement request sequence --- src/supervisor/agents/codex/acp.ts | 18 ++++++---- src/supervisor/agents/codex/codex.test.ts | 40 +++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/src/supervisor/agents/codex/acp.ts b/src/supervisor/agents/codex/acp.ts index 0e25b7ae..52331025 100644 --- a/src/supervisor/agents/codex/acp.ts +++ b/src/supervisor/agents/codex/acp.ts @@ -344,6 +344,9 @@ export class CodexStructuredSession implements StructuredSessionHandle { ): Promise { switch (command.kind) { case "set": + if (this.ensureMapperState().goalItemId) { + await this.rpc.request("thread/goal/clear", { threadId }); + } await this.rpc.request("thread/goal/set", { threadId, objective: command.objective, @@ -369,12 +372,15 @@ export class CodexStructuredSession implements StructuredSessionHandle { async controlGoal(control: ThreadGoalControl): Promise { const threadId = await this.waitForRemoteThreadId(); - await this.dispatchCodexGoalCommand( - threadId, - control.action === "edit" - ? { kind: "set", objective: control.objective } - : { kind: control.action }, - ); + if (control.action === "edit") { + await this.rpc.request("thread/goal/set", { + threadId, + objective: control.objective, + status: "active", + }); + return; + } + await this.dispatchCodexGoalCommand(threadId, { kind: control.action }); } private updateSlashCommands(commands: AgentSlashCommand[]): void { diff --git a/src/supervisor/agents/codex/codex.test.ts b/src/supervisor/agents/codex/codex.test.ts index 87faca5a..480b2d3f 100644 --- a/src/supervisor/agents/codex/codex.test.ts +++ b/src/supervisor/agents/codex/codex.test.ts @@ -1573,6 +1573,46 @@ describe("CodexStructuredSession", () => { expect(updates).not.toContainEqual({ status: "idle", attention: "none" }); }); + it("clears an existing goal before /goal replaces it", async () => { + const requests: Array<{ method: string; params: Record }> = []; + const structuredSession = makeStructuredSession(requests); + dispatchNotification(structuredSession, { + jsonrpc: "2.0", + method: "thread/goal/updated", + params: { + threadId: "provider-thread", + turnId: null, + goal: { + threadId: "provider-thread", + objective: "previous goal", + status: "active", + tokenBudget: null, + tokensUsed: 7_900_000, + timeUsedSeconds: 29_580, + createdAt: 1778570000, + updatedAt: 1778599580, + }, + }, + }); + + await structuredSession.startTurn("/goal start fresh", { model: "gpt-5.4" }); + + expect(requests).toEqual([ + { + method: "thread/goal/clear", + params: { threadId: "provider-thread" }, + }, + { + method: "thread/goal/set", + params: { + threadId: "provider-thread", + objective: "start fresh", + status: "active", + }, + }, + ]); + }); + it("settles /goal when Codex does not start a model turn", async () => { const requests: Array<{ method: string; params: Record }> = []; const structuredSession = makeStructuredSession(requests);