Skip to content

feat: geometry-neutral attach + session-owned geometry - #92

Closed
schickling-assistant wants to merge 1 commit into
compoundingtech:mainfrom
schickling-assistant:fractal-neutral-attach
Closed

feat: geometry-neutral attach + session-owned geometry#92
schickling-assistant wants to merge 1 commit into
compoundingtech:mainfrom
schickling-assistant:fractal-neutral-attach

Conversation

@schickling-assistant

@schickling-assistant schickling-assistant commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

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 attach disturbs the running child in two ways:

  1. min-wins geometry — an attaching client renegotiates the child's size
    (smallest-writable-client-wins), reflowing the running TUI to the attacher's
    terminal.
  2. nudge SIGWINCH — every attach nudges the child (resize by 1 column and
    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 < 4 and decodeSize
reads 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). A
neutral client forwards input (it is 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 — that is what makes N heterogeneous viewers safe.

Session-owned geometry (pty run --size <cols>x<rows>). A session authored
with --size pins its geometry: negotiateSize() becomes a no-op and only an
authoritative pty resize <name> <cols>x<rows> changes it (a single legitimate
SIGWINCH, 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() gains clients.neutral and geometry.{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 leaves readonly false (DATA still writes), still
assigns attachSeq, and keeps the snapshot path — so the observer still gets a
correct 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):

  • Backward-compat — a legacy 4-byte ATTACH on the patched daemon is
    byte-for-byte identical to the unpatched daemon (same resize + nudge).
  • Neutral attach — session geometry unchanged, zero child output after
    attach (no SIGWINCH), input still delivered and echoed.
  • Server-side defense — a neutral client that sends RESIZE anyway still does
    not move the child.
  • Session-owned geometry — a pinned session reports geometry.owner=session;
    neutral and legacy attach both fail to move it; an authoritative pty resize
    moves it with exactly one SIGWINCH.
  • 80×24 freeze — no --size / no tty ⇒ 80×24; --size 160x50 ⇒ the child
    reports the pinned size.

tsc -p tsconfig.build.json is clean.

Compatibility

  • New client + old daemon: the daemon ignores byte 4 and resizes/nudges as
    before (no silent breakage of the wire; the new geometry / clients.neutral
    stats fields let a client detect this and fall back).
  • Old client + new daemon: legacy 4-byte ATTACH, unchanged behaviour.
  • Multi-writer semantics are unchanged: two non-neutral writers still min-wins
    each other. The neutral flag only makes observers inert.

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>
@schickling-assistant

Copy link
Copy Markdown
Contributor Author

Superseded by #96, with the problem and evidence captured separately in #95.

The replacement is rebased on current main and narrows the scope to geometry-neutral interactive attach only; it removes pinned session geometry and authoritative resize from the proposal.

Posted on behalf of @schickling
field value
agent_name ♾️ co3-abyss
agent_session_id 9fb411d4-330d-4635-a497-b236cbe0869c
agent_tool Codex CLI
agent_tool_version 0.144.1
agent_runtime Codex CLI 0.144.1
agent_model unknown
runtime_profile /nix/store/1n76sy6i9y3ki3k4w04jksqvygw67sj0-coding-agent-runtime-profile/share/coding-agents/profile.json
skills_manifest /nix/store/jn6r0r7r59x3a525jch4pwzwykszqp60-agent-skills-corpus/share/agent-skills/manifest.json
worktree pty/schickling-assistant/geometry-neutral-attach
machine dev3
tooling_profile dotfiles@4eac376

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