Fix Codex MCP duplicate user/assistant message emissions

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 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-25 16:16:19 +07:00
co-authored by Claude Opus 4.5
parent 27b0a2da63
commit b6f0cc5c9c
2 changed files with 9 additions and 11 deletions
@@ -402,7 +402,7 @@ function getConversationIdFromMetadata(metadata: unknown): string | undefined {
describe("CodexMcpAgentClient (MCP integration)", () => { describe("CodexMcpAgentClient (MCP integration)", () => {
test( 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 () => { async () => {
const cwd = tmpCwd(); const cwd = tmpCwd();
const restoreSessionDir = useTempCodexSessionDir(); const restoreSessionDir = useTempCodexSessionDir();
@@ -422,7 +422,8 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
try { try {
session = await client.createSession(config); 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"; const prompt = "Say hello";
for await (const event of session.stream(prompt)) { 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 // Provider should NOT emit user_message - that's handled by agent-manager.recordUserMessage()
expect(userMessages.length).toBe(1); // This prevents duplicate user messages when running through the full stack.
expect(userMessages[0].type).toBe("user_message"); expect(userMessages.length).toBe(0);
expect(userMessages[0].text).toBe(prompt);
// CRITICAL: There should be exactly ONE assistant_message event (not duplicated) // CRITICAL: There should be exactly ONE assistant_message event (not duplicated)
expect(assistantMessages.length).toBe(1); expect(assistantMessages.length).toBe(1);
@@ -2596,11 +2596,9 @@ class CodexMcpAgentSession implements AgentSession {
this.currentAbortController = abortController; this.currentAbortController = abortController;
const promptText = toPromptText(prompt); const promptText = toPromptText(prompt);
this.emitEvent({ // NOTE: user_message is NOT emitted here because the agent-manager's
type: "timeline", // recordUserMessage() already handles emitting the user message timeline
provider: CODEX_PROVIDER, // event before calling stream(). Emitting here would cause duplicates.
item: { type: "user_message", text: promptText },
});
void this.forwardPrompt(promptText, options, abortController.signal).catch((error) => { void this.forwardPrompt(promptText, options, abortController.signal).catch((error) => {
const message = error instanceof Error ? error.message : String(error); const message = error instanceof Error ? error.message : String(error);