Context
Byte-offset scrollback resume (#136) makes the client resume from a raw byte offset it snaps to
the server's attached.next. When a session's PTY is respawned in place (PtyHostService
replaces the entry: kill(sessionId, false) with notify=false, then installs a fresh
OutputBuffer starting at offset 0), a reconnecting client detects the rewind because the new
attached.base is below the offset it already consumed, and resets its terminal.
Problem
Rewind detection is currently inferred from offset arithmetic (base < prevReceived). That works
for the common case but is fragile at the boundaries: a brand-new stream that happens to have
produced enough bytes, or a client whose consumed offset is coincidentally near a new stream's
base, can confuse "same stream, resumed" with "new stream, rewound." There is no explicit
identity for which PTY stream an offset belongs to.
Proposal
Give each PTY spawn a monotonic stream epoch (increment on every spawn/respawn for a session).
Carry it in the attached control frame alongside the offsets. The client treats
epoch !== lastEpoch as an authoritative "different stream — reset and resume from this epoch's
next," independent of offset comparison. Offsets remain per-epoch raw byte counts.
Acceptance criteria
Refs
Builds on #136 (attached{base,gap,next,hasReplay} handshake). Files:
maestro-server/src/application/services/PtyHostService.ts,
maestro-server/src/infrastructure/websocket/PtyWebSocketServer.ts,
maestro-ui/src/platform/ptyProtocol.ts, maestro-ui/src/platform/terminal.ts.
Context
Byte-offset scrollback resume (#136) makes the client resume from a raw byte offset it snaps to
the server's
attached.next. When a session's PTY is respawned in place (PtyHostServicereplaces the entry:
kill(sessionId, false)withnotify=false, then installs a freshOutputBufferstarting at offset 0), a reconnecting client detects the rewind because the newattached.baseis below the offset it already consumed, and resets its terminal.Problem
Rewind detection is currently inferred from offset arithmetic (
base < prevReceived). That worksfor the common case but is fragile at the boundaries: a brand-new stream that happens to have
produced enough bytes, or a client whose consumed offset is coincidentally near a new stream's
base, can confuse "same stream, resumed" with "new stream, rewound." There is no explicitidentity for which PTY stream an offset belongs to.
Proposal
Give each PTY spawn a monotonic stream epoch (increment on every spawn/respawn for a session).
Carry it in the
attachedcontrol frame alongside the offsets. The client treatsepoch !== lastEpochas an authoritative "different stream — reset and resume from this epoch'snext," independent of offset comparison. Offsets remain per-epoch raw byte counts.Acceptance criteria
PtyHostServiceassigns a monotonically increasing epoch per session spawn/respawn, exposedvia
getReplay/ the attach path.attachedframe carriesepoch;ptyProtocol.parseControlFrameparses it(back-compatible: absent
epochbehaves as today).base/offsetarithmetic; a same-epoch reconnect never resets purely due to offset coincidence.
retained offset resumes without reset; an absent-epoch server still works (back-compat).
Refs
Builds on #136 (
attached{base,gap,next,hasReplay}handshake). Files:maestro-server/src/application/services/PtyHostService.ts,maestro-server/src/infrastructure/websocket/PtyWebSocketServer.ts,maestro-ui/src/platform/ptyProtocol.ts,maestro-ui/src/platform/terminal.ts.