tui: re-hydrate terminal histories on daemon reconnect (fix blank codex on restart)#378
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
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-memoryItemHistoryand 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.logacross 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.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