## Summary - preserve baseline streaming behavior (smooth mode still commits one line per 50ms tick) - extract adaptive chunking policy and commit-tick orchestration from ChatWidget into `streaming/chunking.rs` and `streaming/commit_tick.rs` - add hysteresis-based catch-up behavior with bounded batch draining to reduce queue lag without bursty single-frame jumps - document policy behavior, tuning guidance, and debug flow in rustdoc + docs ## Testing - just fmt - cargo test -p codex-tui
4.0 KiB
TUI Stream Chunking
This document explains how stream chunking in the TUI works and why it is implemented this way.
Problem
Streaming output can arrive faster than a one-line-per-tick animation can show it. If commit speed stays fixed while arrival speed spikes, queued lines grow and visible output lags behind received output.
Design goals
- Preserve existing baseline behavior under normal load.
- Reduce display lag when backlog builds.
- Keep output order stable.
- Avoid abrupt single-frame flushes that look jumpy.
- Keep policy transport-agnostic and based only on queue state.
Non-goals
- The policy does not schedule animation ticks.
- The policy does not depend on upstream source identity.
- The policy does not reorder queued output.
Where the logic lives
codex-rs/tui/src/streaming/chunking.rs- Adaptive policy, mode transitions, and drain-plan selection.
codex-rs/tui/src/streaming/commit_tick.rs- Orchestration for each commit tick: snapshot, decide, drain, trace.
codex-rs/tui/src/streaming/controller.rs- Queue/drain primitives used by commit-tick orchestration.
codex-rs/tui/src/chatwidget.rs- Integration point that invokes commit-tick orchestration and handles UI lifecycle events.
Runtime flow
On each commit tick:
- Build a queue snapshot across active controllers.
queued_lines: total queued lines.oldest_age: max age of the oldest queued line across controllers.
- Ask adaptive policy for a decision.
- Output: current mode and a drain plan.
- Apply drain plan to each controller.
- Emit drained
HistoryCells for insertion by the caller. - Emit trace logs for observability.
In CatchUpOnly scope, policy state still advances, but draining is skipped
unless mode is currently CatchUp.
Modes and transitions
Two modes are used:
Smooth- Baseline behavior: one line drained per baseline commit tick.
- Baseline tick interval currently comes from
tui/src/app.rs:COMMIT_ANIMATION_TICK(~8.3ms, ~120fps).
CatchUp- Drain current queued backlog per tick via
Batch(queued_lines).
- Drain current queued backlog per tick via
Entry and exit use hysteresis:
- Enter
CatchUpwhen queue depth or queue age exceeds enter thresholds. - Exit requires both depth and age to be below exit thresholds for a hold
window (
EXIT_HOLD).
This prevents oscillation when load hovers near thresholds.
Current experimental tuning values
These are the current values in streaming/chunking.rs plus the baseline
commit tick in tui/src/app.rs. They are
experimental and may change as we gather more trace data.
- Baseline commit tick:
~8.3ms(COMMIT_ANIMATION_TICKinapp.rs) - Enter catch-up:
queued_lines >= 8ORoldest_age >= 120ms
- Exit catch-up eligibility:
queued_lines <= 2ANDoldest_age <= 40ms
- Exit hold (
CatchUp -> Smooth):250ms - Re-entry hold after catch-up exit:
250ms - Severe backlog thresholds:
queued_lines >= 64ORoldest_age >= 300ms
Drain planning
In Smooth, plan is always Single.
In CatchUp, plan is Batch(queued_lines), which drains the currently queued
backlog for immediate convergence.
Why this design
This keeps normal animation semantics intact, while making backlog behavior adaptive:
- Under normal load, behavior stays familiar and stable.
- Under pressure, queue age is reduced quickly without sacrificing ordering.
- Hysteresis avoids rapid mode flapping.
Invariants
- Queue order is preserved.
- Empty queue resets policy back to
Smooth. CatchUpexits only after sustained low pressure.- Catch-up drains are immediate while in
CatchUp.
Observability
Trace events are emitted from commit-tick orchestration:
stream chunking commit tickmode,queued_lines,oldest_queued_age_ms,drain_plan,has_controller,all_idle
stream chunking mode transitionprior_mode,new_mode,queued_lines,oldest_queued_age_ms,entered_catch_up
These events are intended to explain display lag by showing queue pressure, selected drain behavior, and mode transitions over time.