Skip to content

Bug: Codex commentary messages are streamed as agent_message_chunk #169

@burone

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions