ACP Command Payload Flow

This shows the exact sequence for one ACP shell command. Each row answers one question: what object exists now, who receives it, and what changed from the previous row.

Clean HEAD behavior
ACP emits the event harnesses/acp-runner.js:711
Dispatcher routes it harnesses/harness-runtime-event-dispatcher.js:75, 366
Agent IO wraps it conversation/build-agent-io-hooks.js:280
WhatsApp renders/sends it whatsapp/outbound/send-content.js:702, 714, 1841 whatsapp/message-payloads.js:10
1

ACP creates a runtime event

The runner does not emit a bare command object. It emits a harness runtime event.

Source: harnesses/acp-runner.js:711

Object at this point

{
  type: "command.started",
  provider: "acp",
  command: {
    command: "pnpm test",
    status: "started"
  },
  raw: { message }
}
What changed?
  • This is the first object.
  • provider starts as "acp".
2

The dispatcher chooses the ACP route

Because it is ACP and a command runtime event, it stays on the runtime-event path.

Source: harnesses/harness-runtime-event-dispatcher.js:75, 366

Routing decision

if (
  event.provider === "acp" &&
  event.type === "command.started"
) {
  hooks.onRuntimeEvent(event);
  return;
}
What changed?
  • The object is not reformatted yet.
  • hooks.onCommand(...) is skipped.
3

Agent IO wraps it for outbound delivery

The original ACP event is placed inside an app-level outbound event.

Source: conversation/build-agent-io-hooks.js:280 and outbound-events.js:92

New wrapper object

{
  kind: "runtime_event",
  event: {
    type: "command.started",
    provider: "acp",
    command: {
      command: "pnpm test",
      status: "started"
    },
    raw: { message }
  }
}
What changed?
  • kind: "runtime_event" is added.
  • provider: "acp" is still unchanged.
  • Clean HEAD adds no compact and no cwd here.
4

WhatsApp turn IO sends or queues it

The outbound event crosses the transport boundary before rendering to a WhatsApp payload.

Source: execute-action-context.js:22, whatsapp/inbound/chat-turn.js:394, whatsapp/outbound/persistent-queue.js:108

Call chain

context.send(outboundEvent)
// same as
turn.io.send(outboundEvent)

sendOrQueueWhatsAppEvent({
  event: outboundEvent,
  chatId,
  getSocket
})
What changed?
  • The object is still the same outbound event.
  • The transport may send now or persist for replay.
5

The runtime command renderer creates text

Only now does the command become the visible WhatsApp text string.

Source: whatsapp/outbound/send-content.js:702, 714

String transformation

command = "pnpm test"
summary = "*Shell*  `pnpm test`"
icon = "🔧"

text = "🔧 *Shell*  `pnpm test`"
What changed?
  • The runtime event is rendered into a string.
  • *Shell* is the exact payload text, not _Shell_.
6

Baileys receives the final message payload

The text string is wrapped with URL previews disabled.

Source: whatsapp/outbound/send-content.js:1841 and whatsapp/message-payloads.js:10

Final payload

{
  text: "🔧 *Shell*  `pnpm test`",
  linkPreview: null
}
🔧 Shell  pnpm test
What changed?
  • makeTextMessage(text) adds linkPreview: null.
  • This object is passed to sock.sendMessage(...).

ACP Path

Starts as{ type: "command.started", provider: "acp", ... }
Hook usedhooks.onRuntimeEvent(event)
Wrapper{ kind: "runtime_event", event }
ProviderPreserved as "acp".

Direct onCommand Path

Starts as{ command: "pnpm test", status: "started" }
Hook usedhooks.onCommand(commandEvent)
WrapperruntimeEvent({ type: "command.started", provider: "codex", ... })
ProviderSet to "codex".
The key distinction: ACP command events are already runtime events, so they cross the ACP presentation boundary through onRuntimeEvent. The direct command hook is a separate path that manufactures a Codex runtime event from a bare command object.