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
parent 27b0a2da63
commit b6f0cc5c9c
2 changed files with 9 additions and 11 deletions

View File

@@ -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);

View File

@@ -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);