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 69fa94abd..ad5fbe353 100644 --- a/packages/server/src/server/agent/providers/codex-mcp-agent.ts +++ b/packages/server/src/server/agent/providers/codex-mcp-agent.ts @@ -129,6 +129,16 @@ function extractThreadItemCallId(item: Record): string | undefi ); } +function normalizeThreadEventType(type: string): string { + if (type.startsWith("thread.item.")) { + return type.slice("thread.".length); + } + if (type.startsWith("thread.turn.")) { + return type.slice("thread.".length); + } + return type; +} + function buildCommandDisplayName(command?: unknown): string { if (typeof command === "string") { const trimmed = command.trim(); @@ -969,13 +979,22 @@ class CodexMcpAgentSession implements AgentSession { if (!event || typeof event !== "object") { return; } - const type = (event as { type?: unknown }).type; - if (typeof type !== "string") { + const record = event as Record; + const data = record.data && typeof record.data === "object" ? (record.data as Record) : null; + const recordType = record.type; + const dataType = data?.type; + let type = typeof recordType === "string" ? recordType : undefined; + let eventRecord: Record = record; + if ((!type || type === "event") && typeof dataType === "string") { + type = dataType; + eventRecord = { ...record, ...data, type }; + } + if (!type) { return; } if (type.includes(".") || type.startsWith("turn.") || type.startsWith("thread.") || type.startsWith("item.")) { - this.handleThreadEvent(event as Record); + this.handleThreadEvent(eventRecord); return; } @@ -1163,10 +1182,14 @@ class CodexMcpAgentSession implements AgentSession { } private handleThreadEvent(event: Record): void { - const type = event.type as string; + const rawType = event.type as string; + const type = normalizeThreadEventType(rawType); + const data = + event.data && typeof event.data === "object" ? (event.data as Record) : null; switch (type) { case "thread.started": { - const threadId = event.thread_id as string | undefined; + const threadId = + (event.thread_id as string | undefined) ?? (data?.thread_id as string | undefined); if (threadId) { this.sessionId = threadId; this.flushPendingHistory(); @@ -1182,19 +1205,26 @@ class CodexMcpAgentSession implements AgentSession { this.emitEvent({ type: "turn_started", provider: "codex-mcp" }); break; case "turn.completed": { - const usage = this.convertUsage((event as { usage?: unknown }).usage); + const usage = this.convertUsage( + (event as { usage?: unknown }).usage ?? (data as { usage?: unknown } | null)?.usage + ); this.emitEvent({ type: "turn_completed", provider: "codex-mcp", usage }); break; } case "turn.failed": { - const error = (event as { error?: { message?: string } }).error?.message ?? "Codex MCP turn failed"; + const errorRecord = + (event as { error?: { message?: string } }).error ?? + ((data as { error?: { message?: string } } | null)?.error ?? null); + const error = errorRecord?.message ?? "Codex MCP turn failed"; this.emitEvent({ type: "turn_failed", provider: "codex-mcp", error }); break; } case "item.started": case "item.updated": case "item.completed": { - const item = event.item as Record | undefined; + const item = + (event.item as Record | undefined) ?? + (data?.item as Record | undefined); if (!item) break; const timelineItem = this.threadItemToTimeline(item); if (timelineItem) { @@ -1218,7 +1248,10 @@ class CodexMcpAgentSession implements AgentSession { break; } case "error": { - const message = (event.message as string | undefined) ?? "Codex MCP stream error"; + const message = + (event.message as string | undefined) ?? + (data?.message as string | undefined) ?? + "Codex MCP stream error"; this.emitEvent({ type: "timeline", provider: "codex-mcp", diff --git a/plan.md b/plan.md index ae7ba536b..b05c9e8ed 100644 --- a/plan.md +++ b/plan.md @@ -118,12 +118,22 @@ Build a new Codex MCP provider side‑by‑side with the existing Codex SDK prov - [x] **Fix**: Codex MCP command output should include exit codes for command tool calls. - **Done (2025-12-24 19:36)**: Ensured command tool outputs include exit codes even when output text is missing. -- [ ] **Fix**: Codex MCP thread/item event mapping should capture file_change, mcp_tool_call, web_search, and todo_list. +- [x] **Fix**: Codex MCP thread/item event mapping should capture file_change, mcp_tool_call, web_search, and todo_list. + - **Done (2025-12-24 19:40)**: Normalized thread event types and added data fallback so item events map to timeline entries. - [ ] **Fix**: Codex MCP should emit error timeline items for failed turns. - [ ] **Fix**: Codex MCP persistence should include conversationId metadata for resume. +- [ ] **Investigate**: Elicitation was fixed in Codex 0.71.0 - why isn't it working for us? + + - Web search found: Issue #6992 - Codex auto-declined elicitation requests, fixed in 0.71.0 + - We're on 0.77.0 so elicitation SHOULD work now + - The bug was in `codex-rs/rmcp-client/src/logging_client_handler.rs` + - Verify we're actually receiving ElicitRequest but maybe handling it wrong + - Check GitHub issue #6992 for exact fix details + - Maybe happy-cli works because it's on a newer version? + - [ ] **Fix**: Codex MCP permission elicitation should surface permission_requested/resolved events (read-only/untrusted too). - [ ] **Fix**: Codex SDK persistence hydration should include completed shell_command tool entries.