feat(maestro): persist PTY scrollback across a reboot
A resumed session opened on a blank pane: the agent's conversation came back, the terminal history the operator was reading did not. Each PTY bridge's replay ring is now mirrored to `<state_dir>/scrollback/<id>.bin` and handed to the new bridge on resume, and on the recovery pass that re-adopts a session tmux still holds — that one lost its ring with the previous daemon too. Restored bytes go out ahead of the live ones, and are dropped once the ring holds a full budget of new output of its own. Flushed every 10 seconds by the liveness watcher, and once more when a bridge is sealed on exit. The timer is the part that matters: a host reboot runs no exit path at all, so a scrollback written only on a clean shutdown is missing in exactly the case this exists for. Ten seconds bounds the loss to the last few lines — this is visual continuity, not state — and costs at most one file write per active session per tick, since a session whose bridge has read nothing since the last flush is skipped. The file is capped at the existing `pty_replay_bytes` budget rather than a second one, is deleted on kill and when recovery prunes a sidecar, and a missing, unreadable or oversized one degrades to an empty pane instead of failing the resume. Closes #171 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
e7830d15b0
commit
5a47f6fbf6
@@ -108,6 +108,32 @@ dormant ghost on the next recovery. The other thing to do with a dormant
|
||||
session is dismiss it, which removes the sidecar so the next recovery does not
|
||||
bring it back.
|
||||
|
||||
## What the pane looks like when it comes back
|
||||
|
||||
The PTY replay ring lives in memory, so a resumed session would otherwise open
|
||||
on a blank pane: the conversation is back, the terminal history the operator
|
||||
was reading is not. Each bridge's ring is mirrored to
|
||||
`<state_dir>/scrollback/<id>.bin` (#171) and handed to the new bridge on
|
||||
resume, and on the recovery pass that re-adopts a session tmux still holds —
|
||||
that one lost its ring with the previous daemon too. The restored bytes go out
|
||||
ahead of the live ones, and are dropped once the ring holds a full budget of
|
||||
new output of its own, which is the point where everything restored is older
|
||||
than the budget covers.
|
||||
|
||||
The file is capped at the same `[daemon] pty_replay_bytes` budget the ring
|
||||
uses — one budget, not two — and it is deleted when the session is killed, and
|
||||
when recovery prunes its sidecar. Nothing here can fail a resume: a missing,
|
||||
unreadable or oversized file degrades to an empty pane, which is exactly what
|
||||
the operator had before it existed.
|
||||
|
||||
It is flushed every 10 seconds by the liveness watcher, and once more when a
|
||||
session's bridge is sealed on exit. The timer is the part that matters: a host
|
||||
reboot runs no exit path at all, so scrollback written only on a clean shutdown
|
||||
would be missing in the one case this exists for. Ten seconds bounds the loss
|
||||
to the last few lines — scrollback is visual continuity, not state — and costs
|
||||
at most one file write per *active* session per tick, since a session whose
|
||||
bridge has read nothing since the last flush is skipped.
|
||||
|
||||
## What recovery does with them: `resume_on_boot`
|
||||
|
||||
Whether a recovered-but-dead session is left dormant at all is the operator's
|
||||
|
||||
Reference in New Issue
Block a user