Desktop app-server should coalesce renderer-facing delta notifications to reduce event storms
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-enabledand 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.sqlitehad grown to roughly952 MBon 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
AgentMessageDeltaonly whenthread_id,turn_id, anditem_idall match; - coalesce
CommandExecutionOutputDeltaonly whenthread_id,turn_id, anditem_idall 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