Desktop app-server should coalesce renderer-facing delta notifications to reduce event storms

Open 💬 0 comments Opened Jun 28, 2026 by code-yeongyu

Summary

Codex Desktop can overwhelm the renderer with very high-frequency, tiny app-server delta notifications during long/tool-heavy turns. On macOS I observed the renderer becoming the hot process while the app-server and MCP bridge processes were still alive and mostly low CPU.

The likely problem is not that every bridge/tool process dies. It is that the renderer has to parse, reconcile, render, and persist too many small event updates.

Local evidence

During the freeze investigation:

  • Codex Desktop was running /Applications/Codex.app/Contents/Resources/codex app-server --analytics-default-enabled and stdio app-server children.
  • Renderer processes were the visible hot path during the UI freeze.
  • The app-server and bridge/helper processes remained alive.
  • ~/.codex/logs_2.sqlite had grown to roughly 952 MB on the affected profile.
  • Local logs/state showed many stream/delta-oriented events around agent text, command output, SSE/websocket/app-server traffic.

I intentionally omitted raw logs and full process command lines because they can contain private paths, local session text, or capability-token file paths.

Concrete suspected bug

The app-server transport currently forwards renderer-facing deltas one notification at a time. In a bursty stream this can mean thousands of JSON-RPC notifications such as:

AgentMessageDelta("h")
AgentMessageDelta("e")
AgentMessageDelta("l")
AgentMessageDelta("l")
AgentMessageDelta("o")

Each notification can cause renderer-side JSON parsing, state reconciliation, transcript updates, and log/state persistence. Long active sessions and tool-heavy runs amplify the cost.

Suggested fix

Coalesce adjacent renderer-facing delta notifications before writing them to the Desktop client:

  • coalesce AgentMessageDelta only when thread_id, turn_id, and item_id all match;
  • coalesce CommandExecutionOutputDelta only when thread_id, turn_id, and item_id all match;
  • preserve order by appending delta strings;
  • flush before non-delta notifications such as item completion;
  • flush when the stream identity changes;
  • do not coalesce messages that carry write-completion acknowledgements;
  • avoid delaying sparse/slow streams: drain only the currently queued burst and flush when the writer queue becomes empty;
  • consider the same invariant for websocket/remote-control transports if they have the same renderer-facing event pressure.

A local prototype in codex-rs/app-server-transport used this shape:

before: 3 queued adjacent deltas -> 3 stdout JSON lines
 after: 3 queued adjacent deltas -> 1 stdout JSON line with concatenated payload

Validation from local prototype

The prototype was not installed into the signed Desktop bundle, but focused Rust tests passed against the transport package:

cargo test -p codex-app-server-transport outgoing_message_coalescer
# 8 passed

cargo test -p codex-app-server-transport transport::stdio
# 3 passed

The tests covered:

  • adjacent agent-message delta coalescing;
  • adjacent command-output delta coalescing;
  • thread/turn/item boundary preservation;
  • flushing before non-delta notifications;
  • preserving write-completion ack behavior;
  • reducing burst stdout writes;
  • flushing sparse deltas when the writer queue drains.

Expected behavior

A burst of renderer-facing text/output deltas should result in fewer client writes/events while preserving the exact user-visible text and event ordering.

Desktop should remain responsive during long streaming turns because the renderer receives a bounded event rate instead of every tiny stream fragment as a separate UI event.

Related issues

This is probably a narrower fix surface under the broader Desktop renderer/state pressure family, related to:

  • #28502
  • #25779
  • #26362
  • #21134
  • #24275

View original on GitHub ↗