Skip to content

Bug: Codex commentary messages are streamed as agent_message_chunk #169

Closed

Description

@burone

Summary

@agentclientprotocol/codex-acp currently streams Codex agentMessage items with phase: "commentary" as normal assistant body text (agent_message_chunk) instead of thought/progress text (agent_thought_chunk).

This causes ACP clients that render thoughts separately to display Codex commentary/progress in the main assistant message body.

Expected Behavior

Codex agent messages should be mapped by phase:

  • phase: "commentary" -> agent_thought_chunk
  • phase: "final_answer" -> agent_message_chunk
  • phase: null / unknown -> agent_message_chunk

Actual Behavior

Both commentary and final answer text deltas are emitted as:

{
  "sessionUpdate": "agent_message_chunk",
  "content": {
    "type": "text",
    "text": "I will inspect the workspace."
  }
}

Root Cause

Codex provides the phase on item/started:

{
  "method": "item/started",
  "params": {
    "item": {
      "type": "agentMessage",
      "id": "msg_...",
      "text": "",
      "phase": "commentary"
    }
  }
}

But subsequent text deltas only contain itemId and delta:

{
  "method": "item/agentMessage/delta",
  "params": {
    "itemId": "msg_...",
    "delta": "I"
  }
}

The current implementation handles item/agentMessage/delta without remembering the phase from item/started, so all deltas become agent_message_chunk.

Impact

ACP clients cannot distinguish Codex commentary/progress from final assistant text.

For example, in our WeCom integration, Codex progress text such as:

I will inspect the workspace.

is rendered in the main card body instead of the card's thinking/progress section.

Suggested Fix

Track the agentMessage phase by item.id when receiving item/started, then use that phase when handling item/agentMessage/delta.

private readonly activeAgentMessagePhases = new Map<string, MessagePhase | null>();

// item/started
if (event.item.type === "agentMessage") {
  this.activeAgentMessagePhases.set(event.item.id, event.item.phase);
  return null;
}

// item/agentMessage/delta
const phase = this.activeAgentMessagePhases.get(event.itemId);

return {
  sessionUpdate:
    phase === "commentary" ? "agent_thought_chunk" : "agent_message_chunk",
  content: {
    type: "text",
    text: event.delta,
  },
};

// item/completed
if (event.item.type === "agentMessage") {
  this.activeAgentMessagePhases.delete(event.item.id);
  return null;
}

History replay should also respect item.phase:

case "agentMessage":
  return [{
    sessionUpdate:
      item.phase === "commentary" ? "agent_thought_chunk" : "agent_message_chunk",
    content: {
      type: "text",
      text: item.text,
    },
  }];

Version

Observed in:

@agentclientprotocol/codex-acp@0.0.44

Activity

  1. benbrandt commented on Jun 26, 2026

    @benbrandt
    Member

    As I understand this, the current behavior seems correct to me, as we get separate reasoning entries already.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions