diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts index da7386c098..eaea9ba3a0 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.test.ts @@ -118,4 +118,27 @@ describe("Codex app-server provider", () => { }), ).toBe(true); }); + + test("only synthesizes completion from idle thread status while a run is active", () => { + expect( + __codexAppServerInternals.shouldSynthesizeTurnCompletionFromThreadStatus({ + statusType: "idle", + hasActiveRun: true, + }), + ).toBe(true); + + expect( + __codexAppServerInternals.shouldSynthesizeTurnCompletionFromThreadStatus({ + statusType: "idle", + hasActiveRun: false, + }), + ).toBe(false); + + expect( + __codexAppServerInternals.shouldSynthesizeTurnCompletionFromThreadStatus({ + statusType: "running", + hasActiveRun: true, + }), + ).toBe(false); + }); }); diff --git a/packages/server/src/server/agent/providers/codex-app-server-agent.ts b/packages/server/src/server/agent/providers/codex-app-server-agent.ts index 4c21d8df44..6cd509a593 100644 --- a/packages/server/src/server/agent/providers/codex-app-server-agent.ts +++ b/packages/server/src/server/agent/providers/codex-app-server-agent.ts @@ -1195,6 +1195,18 @@ const ThreadStartedNotificationSchema = z }) .passthrough(); +const ThreadStatusChangedNotificationSchema = z + .object({ + threadId: z.string().optional(), + status: z + .object({ + type: z.string().optional(), + }) + .passthrough() + .optional(), + }) + .passthrough(); + const TurnStartedNotificationSchema = z .object({ turn: z.object({ id: z.string() }).passthrough(), @@ -1402,6 +1414,16 @@ const ItemFileChangeOutputDeltaNotificationSchema = z }) .passthrough(); +const ItemCommandExecutionOutputDeltaNotificationSchema = z + .object({ + itemId: z.string().optional(), + callId: z.string().optional(), + stream: z.string().optional(), + delta: z.string().optional(), + chunk: z.string().optional(), + }) + .passthrough(); + const CodexEventTurnDiffNotificationSchema = z .object({ msg: z @@ -1416,6 +1438,7 @@ const CodexEventTurnDiffNotificationSchema = z type ParsedCodexNotification = | { kind: "thread_started"; threadId: string } + | { kind: "thread_status_changed"; threadId: string | null; statusType: string | null } | { kind: "turn_started"; turnId: string } | { kind: "turn_completed"; status: string; errorMessage: string | null } | { kind: "plan_updated"; plan: Array<{ step: string | null; status: string | null }> } @@ -1499,6 +1522,25 @@ const CodexNotificationSchema = z.union([ params, }), ), + z + .object({ + method: z.literal("thread/status/changed"), + params: ThreadStatusChangedNotificationSchema, + }) + .transform( + ({ params }): ParsedCodexNotification => ({ + kind: "thread_status_changed", + threadId: params.threadId ?? null, + statusType: params.status?.type ?? null, + }), + ), + z.object({ method: z.literal("thread/status/changed"), params: z.unknown() }).transform( + ({ method, params }): ParsedCodexNotification => ({ + kind: "invalid_payload", + method, + params, + }), + ), z .object({ method: z.literal("turn/started"), params: TurnStartedNotificationSchema }) .transform( @@ -1872,6 +1914,28 @@ const CodexNotificationSchema = z.union([ params, }), ), + z + .object({ + method: z.literal("item/commandExecution/outputDelta"), + params: ItemCommandExecutionOutputDeltaNotificationSchema, + }) + .transform( + ({ params }): ParsedCodexNotification => ({ + kind: "exec_command_output_delta", + callId: params.callId ?? params.itemId ?? null, + stream: params.stream ?? null, + chunk: params.delta ?? params.chunk ?? null, + }), + ), + z + .object({ method: z.literal("item/commandExecution/outputDelta"), params: z.unknown() }) + .transform( + ({ method, params }): ParsedCodexNotification => ({ + kind: "invalid_payload", + method, + params, + }), + ), z .object({ method: z.literal("codex/event/turn_diff"), @@ -2035,6 +2099,7 @@ export async function codexAppServerTurnInputFromPrompt( export const __codexAppServerInternals = { mapCodexPatchNotificationToToolCall, shouldBufferCodexEventUntilTurnStarted, + shouldSynthesizeTurnCompletionFromThreadStatus, }; function isTerminalAgentStreamEvent(event: AgentStreamEvent): boolean { @@ -2057,6 +2122,13 @@ function shouldBufferCodexEventUntilTurnStarted(event: AgentStreamEvent): boolea return true; } +function shouldSynthesizeTurnCompletionFromThreadStatus(input: { + statusType: string | null; + hasActiveRun: boolean; +}): boolean { + return input.hasActiveRun && input.statusType === "idle"; +} + class CodexAppServerAgentSession implements AgentSession { readonly provider = CODEX_PROVIDER; readonly capabilities = CODEX_APP_SERVER_CAPABILITIES; @@ -2095,6 +2167,7 @@ class CodexAppServerAgentSession implements AgentSession { private warnedInvalidNotificationPayloads = new Set(); private warnedIncompleteEditToolCallIds = new Set(); private latestUsage: AgentUsage | undefined; + private runAwaitingTerminal = false; private connected = false; private collaborationModes: Array<{ name: string; @@ -2521,6 +2594,7 @@ class CodexAppServerAgentSession implements AgentSession { } await this.client.request("turn/start", params, TURN_START_TIMEOUT_MS); + this.runAwaitingTerminal = true; let sawTurnStarted = false; for await (const event of queue) { @@ -2542,6 +2616,7 @@ class CodexAppServerAgentSession implements AgentSession { } } } finally { + this.runAwaitingTerminal = false; if (this.eventQueue === queue) { this.eventQueue = null; } @@ -2875,6 +2950,31 @@ class CodexAppServerAgentSession implements AgentSession { return; } + if (parsed.kind === "thread_status_changed") { + if ( + shouldSynthesizeTurnCompletionFromThreadStatus({ + statusType: parsed.statusType, + hasActiveRun: this.runAwaitingTerminal, + }) + ) { + this.runAwaitingTerminal = false; + this.currentTurnId = null; + this.emittedItemStartedIds.clear(); + this.emittedItemCompletedIds.clear(); + this.emittedExecCommandStartedCallIds.clear(); + this.emittedExecCommandCompletedCallIds.clear(); + this.pendingCommandOutputDeltas.clear(); + this.pendingFileChangeOutputDeltas.clear(); + this.warnedIncompleteEditToolCallIds.clear(); + this.emitEvent({ + type: "turn_completed", + provider: CODEX_PROVIDER, + usage: this.latestUsage, + }); + } + return; + } + if (parsed.kind === "turn_started") { this.currentTurnId = parsed.turnId; this.emittedItemStartedIds.clear(); @@ -2889,6 +2989,8 @@ class CodexAppServerAgentSession implements AgentSession { } if (parsed.kind === "turn_completed") { + this.runAwaitingTerminal = false; + this.currentTurnId = null; if (parsed.status === "failed") { this.emitEvent({ type: "turn_failed",