feat: geometry-neutral attach + session-owned geometry - #92
Closed
schickling-assistant wants to merge 1 commit into
Closed
feat: geometry-neutral attach + session-owned geometry#92schickling-assistant wants to merge 1 commit into
schickling-assistant wants to merge 1 commit into
Conversation
Add a non-disturbing observer-attach role and an authoritative,
session-owned geometry so multiple viewers at different device sizes can
watch/drive a running TUI without perturbing it.
Two disturbance vectors that a plain `pty attach` causes today are killed:
1. min-wins geometry: an attaching client renegotiates the child's size
(smallest-writable-client-wins), reflowing the running TUI.
2. nudge SIGWINCH: every attach nudges the child (resize by 1 col and
back) to force a redraw, jittering the agent.
Mechanism (backward-compatible, opt-in):
- protocol: optional 5th flags byte on ATTACH/RESIZE payloads. The server
already guards `payload.length < 4` and `decodeSize` reads only bytes
0-3, so a legacy 4-byte frame stays byte-identical and an old server
ignores the new byte. `ATTACH_FLAG_GEOMETRY_NEUTRAL` /
`RESIZE_FLAG_AUTHORITATIVE`.
- server: `Client.geometryNeutral` — a neutral client forwards input
(not readonly) but is inert in `negotiateSize()` and is never nudged.
The skip lives server-side in `negotiateSize()`, so even a misbehaving
neutral client that sends RESIZE cannot move the child.
- `pinnedGeometry` / `setSessionSize()`: a session authored with `--size`
pins its geometry; `negotiateSize()` becomes a no-op and only an
authoritative `pty resize` changes it (a single legitimate SIGWINCH,
no nudge). `collectStats()` gains `clients.neutral` and
`geometry.{owner,pinned}` so clients can capability-gate on them.
- client: `AttachOptions.geometryNeutral` sends the flag and skips
registering the local resize->RESIZE handler.
- cli: `pty run --size <cols>x<rows>` (pinned session, also fixes the
80x24 freeze when spawned without a controlling tty),
`pty attach --no-resize` (alias `--neutral`), and `pty resize <name>
<cols>x<rows>` (authoritative session-owned resize).
Validated end-to-end against running children with a raw-socket harness
(legacy 4-byte vs neutral 5-byte frames) plus child-output-quiescence and
SIGWINCH-count measurement: legacy attach byte-identical to unpatched;
neutral attach delivers input with zero resize/nudge; a pinned session
ignores both neutral and legacy attach geometry; authoritative resize
moves it with exactly one WINCH; `--size` fixes the no-tty 80x24 default.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Superseded by #96, with the problem and evidence captured separately in #95. The replacement is rebased on current Posted on behalf of @schickling
|
This was referenced Jul 20, 2026
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.
Summary
Adds a non-disturbing observer-attach role and an authoritative,
session-owned geometry so multiple viewers at different terminal sizes can
watch (and drive) a running TUI session without perturbing it.
Today a plain
pty attachdisturbs the running child in two ways:(smallest-writable-client-wins), reflowing the running TUI to the attacher's
terminal.
back) to force a redraw, which jitters the agent.
Both are correct for a single interactive attach, but they make it impossible to
have several observers of different sizes, or to keep a headless session at a
stable rendering size. This PR makes those behaviours opt-out per client while
keeping the default path byte-for-byte identical.
What's new
Protocol (backward-compatible). An optional 5th flags byte on ATTACH /
RESIZE payloads. The server already guards
payload.length < 4anddecodeSizereads only bytes 0–3, so a legacy 4-byte frame is unchanged and an old server
ignores the new byte. Two flags:
ATTACH_FLAG_GEOMETRY_NEUTRAL,RESIZE_FLAG_AUTHORITATIVE.Geometry-neutral client (
pty attach --no-resize, alias--neutral). Aneutral client forwards input (it is not readonly) but is inert in
negotiateSize()and is never nudged. The skip lives server-side innegotiateSize(), so even a misbehaving neutral client that sends RESIZE cannotmove the child — that is what makes N heterogeneous viewers safe.
Session-owned geometry (
pty run --size <cols>x<rows>). A session authoredwith
--sizepins its geometry:negotiateSize()becomes a no-op and only anauthoritative
pty resize <name> <cols>x<rows>changes it (a single legitimateSIGWINCH, no nudge). This also fixes the case where a session spawned without a
controlling tty freezes at the daemon's 80×24 default regardless of any later
viewer's terminal.
Stats.
collectStats()gainsclients.neutralandgeometry.{owner,pinned}so clients can capability-gate on the new behaviour (a viewer can detect an
old daemon that would silently disturb and fall back).
The neutral flag gates only (a) participation in
negotiateSize()and(b)
nudgeRedraw(). It leavesreadonlyfalse (DATA still writes), stillassigns
attachSeq, and keeps the snapshot path — so the observer still gets acorrect first-frame snapshot (alt-screen + colour) and the live stream.
Validation
Verified end-to-end against running children with a raw-socket harness that
crafts legacy 4-byte vs neutral 5-byte frames directly (isolating the server),
plus child-output-quiescence and SIGWINCH-count measurement (a quiescent child
that prints only on SIGWINCH/input, so any post-attach byte is unambiguous):
byte-for-byte identical to the unpatched daemon (same resize + nudge).
attach (no SIGWINCH), input still delivered and echoed.
not move the child.
geometry.owner=session;neutral and legacy attach both fail to move it; an authoritative
pty resizemoves it with exactly one SIGWINCH.
--size/ no tty ⇒ 80×24;--size 160x50⇒ the childreports the pinned size.
tsc -p tsconfig.build.jsonis clean.Compatibility
before (no silent breakage of the wire; the new
geometry/clients.neutralstats fields let a client detect this and fall back).
each other. The neutral flag only makes observers inert.