Skip to content

tui: re-hydrate terminal histories on daemon reconnect (fix blank codex on restart)#378

Merged
edwin-zvs merged 3 commits into
mainfrom
fix-codex-resume-redraw
Jun 6, 2026
Merged

tui: re-hydrate terminal histories on daemon reconnect (fix blank codex on restart)#378
edwin-zvs merged 3 commits into
mainfrom
fix-codex-resume-redraw

Conversation

@edwin-zvs

@edwin-zvs edwin-zvs commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Problem

After a daemon restart, a codex session renders blank except its footer (input box + status). A small terminal resize makes the full conversation appear — the content is there, it just isn't painted until something forces a redraw.

What I measured (correcting my first guess)

My first attempt assumed codex was slow to resume and the daemon's force-redraw fired too early. Direct measurement (resuming real rollouts in a PTY, rendering with pyte and the agent's own renderer) disproved that:

  • codex loads + draws its whole conversation in ~0.5s even for a 26 MB rollout — load time is not the issue.
  • codex's resume output renders the full conversation cleanly through the agent's renderer.
  • codex's resume output does NO screen clear — zero ESC[2J/ESC[3J/cursor-home. It assumes a blank terminal.

Root cause

A daemon restart respawns every session and truncates each pty.log (Manager::respawn) so the resumed child renders into a clean slate. But the TUI kept its pre-restart in-memory ItemHistory and fed the resumed child's clean-slate output on top of that stale grid / scroll-region / cursor state → half-rendered, in practice blank. A manual resize forces a full SIGWINCH repaint — which is exactly why resizing "fixes" it. The TUI was the one party that never honored the daemon-side truncate.

Fix

On reconnect (which happens on every daemon restart), clear the in-memory histories so the visible/pinned PTY sessions re-hydrate from the daemon's now-clean pty.log, mirroring the daemon-side truncate. The resumed child's output then lands on an empty history and renders the full conversation on the first frame — no manual resize.

Silent-resume harnesses (zarvis) keep their pty.log across respawn, so re-hydration just reloads the full log (no special-casing).

Also included (defense in depth)

The daemon-side force-redraw for non-silent-resume harnesses now fires when the resumed child's PTY output settles instead of after a fixed 250 ms (which was too short for some resumes). This stays as a backstop and covers single-session restarts, where the client does not reconnect and so does not re-hydrate.

See specs/0023-resumed-pty-rehydrates-from-daemon-not-stale-client-state.md.

Testing

  • cargo test -p agentd-cli -p agentd (reconnect integration + force-redraw + settle gate) — green; full workspace builds clean.
  • Needs your real-session check: run this agentd + agent, open a codex session with a screenful of conversation, restart the daemon, and confirm the pane repaints on its own without a manual resize. The App-layer reconnect+hydration path isn't unit-testable without a full daemon+TUI, so your real environment is the authoritative test.

🤖 Generated with Claude Code

edwin-zvs added 2 commits June 6, 2026 11:23
Codex/claude/shell only repaint past content on SIGWINCH, and the
respawned child is spawned at the cached size, so a same-size pty_resize
from a reconnecting TUI is a kernel no-op. The daemon already forces a
SIGWINCH (bump width by 1, restore) to make the child repaint, but it
did so after a fixed 250ms delay. For codex resuming a large
conversation that delay was too short: the bump landed while only the
banner/footer was drawn, repainted that partial frame, and -- with no
further SIGWINCH -- the pane stayed blank until the user manually
resized.

Fire the force-redraw when the child's PTY output settles (produced
output, then quiet for 400ms) instead of on a fixed delay, with a 6s
hard cap so a child that never settles or never draws still gets a
redraw. Reuses the existing last_pty_at_ms activity timestamp.

Spec 0023.
The reported bug: after a daemon restart a codex session renders blank
except its footer, and a small terminal resize makes the full
conversation appear.

Root cause (measured, not the resume timing I first guessed):
- codex loads + draws its whole conversation in ~0.5s even for a 26MB
  rollout, and its resume output does NO screen clear (zero ESC[2J/3J/
  cursor-home).
- A daemon restart respawns every session and truncates each pty.log so
  the resumed child renders into a clean slate. But the TUI kept its
  pre-restart in-memory ItemHistory and fed the resumed child's
  clean-slate output on top of that stale grid -> half-rendered / blank.
  A manual resize forces a full SIGWINCH repaint, which is why resizing
  'fixes' it.

Fix: on reconnect, clear the in-memory histories so the visible/pinned
PTY sessions re-hydrate from the daemon's (now clean) pty.log, mirroring
the daemon-side truncate. The resumed child's output then lands on an
empty history and renders the full conversation on the first frame.

The settle-timed force-redraw from the previous commit stays as defense
in depth (and for single-session restarts where the client does not
reconnect).

Spec 0023.
@edwin-zvs edwin-zvs changed the title daemon: wait for resumed child to settle before forcing redraw tui: re-hydrate terminal histories on daemon reconnect (fix blank codex on restart) Jun 6, 2026
@edwin-zvs
edwin-zvs merged commit d920d14 into main Jun 6, 2026
1 check passed
@edwin-zvs
edwin-zvs deleted the fix-codex-resume-redraw branch June 6, 2026 20:13
@edwin-zvs edwin-zvs mentioned this pull request Jun 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant