From b6f0cc5c9c82f63d512992dec21c8a07d0ba195c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Thu, 25 Dec 2025 16:16:19 +0700 Subject: [PATCH] Fix Codex MCP duplicate user/assistant message emissions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: user_message was being emitted from TWO places: 1. agent-manager.recordUserMessage() - called by session.ts before stream() 2. codex-mcp-agent.ts stream() method - was emitting its own user_message Fix: Remove user_message emission from codex-mcp-agent.ts stream() since the agent-manager already handles this. Added explanatory comment. Updated test to expect 0 user_messages from provider (agent-manager handles this through the full stack). Verified via Playwright E2E: created new Codex agent, sent "test fix", confirmed only ONE user message appears in UI. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../server/agent/providers/codex-mcp-agent.test.ts | 12 ++++++------ .../src/server/agent/providers/codex-mcp-agent.ts | 8 +++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts b/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts index 127da8a32..9b0a6de3f 100644 --- a/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts +++ b/packages/server/src/server/agent/providers/codex-mcp-agent.test.ts @@ -402,7 +402,7 @@ function getConversationIdFromMetadata(metadata: unknown): string | undefined { describe("CodexMcpAgentClient (MCP integration)", () => { test( - "emits exactly one user_message and one assistant_message per turn", + "provider does not emit user_message (agent-manager handles that), emits exactly one assistant_message", async () => { const cwd = tmpCwd(); const restoreSessionDir = useTempCodexSessionDir(); @@ -422,7 +422,8 @@ describe("CodexMcpAgentClient (MCP integration)", () => { try { session = await client.createSession(config); - // Simple prompt that should result in exactly one user message and one assistant message + // Simple prompt that should result in exactly one assistant message + // NOTE: user_message is NOT emitted by the provider - that's the agent-manager's job const prompt = "Say hello"; for await (const event of session.stream(prompt)) { @@ -440,10 +441,9 @@ describe("CodexMcpAgentClient (MCP integration)", () => { } } - // CRITICAL: There should be exactly ONE user_message event - expect(userMessages.length).toBe(1); - expect(userMessages[0].type).toBe("user_message"); - expect(userMessages[0].text).toBe(prompt); + // Provider should NOT emit user_message - that's handled by agent-manager.recordUserMessage() + // This prevents duplicate user messages when running through the full stack. + expect(userMessages.length).toBe(0); // CRITICAL: There should be exactly ONE assistant_message event (not duplicated) expect(assistantMessages.length).toBe(1); diff --git a/packages/server/src/server/agent/providers/codex-mcp-agent.ts b/packages/server/src/server/agent/providers/codex-mcp-agent.ts index 2520e8d00..4d518aaba 100644 --- a/packages/server/src/server/agent/providers/codex-mcp-agent.ts +++ b/packages/server/src/server/agent/providers/codex-mcp-agent.ts @@ -2596,11 +2596,9 @@ class CodexMcpAgentSession implements AgentSession { this.currentAbortController = abortController; const promptText = toPromptText(prompt); - this.emitEvent({ - type: "timeline", - provider: CODEX_PROVIDER, - item: { type: "user_message", text: promptText }, - }); + // NOTE: user_message is NOT emitted here because the agent-manager's + // recordUserMessage() already handles emitting the user message timeline + // event before calling stream(). Emitting here would cause duplicates. void this.forwardPrompt(promptText, options, abortController.signal).catch((error) => { const message = error instanceof Error ? error.message : String(error);