Skip to content

perf(ws): foreground terminal delivery — connection-local interest scheduling - #706

Merged
danshapiro merged 3 commits into
mainfrom
feat/terminal-foreground-delivery
Sep 5, 2026
Merged

perf(ws): foreground terminal delivery — connection-local interest scheduling#706
danshapiro merged 3 commits into
mainfrom
feat/terminal-foreground-delivery

Conversation

@danshapiro

Copy link
Copy Markdown
Owner

perf(ws): foreground terminal delivery — connection-local interest scheduling

Problem

On top of #705's per-connection writer, ALL queued terminal output competed FIFO-equal for a connection's byte budget. On a constrained link, background CLIs (CI output, test runners, long builds) keep the focused pane's bytes behind milliseconds-to-seconds of unrelated backlog. Nothing knew which terminal the user is actually looking at.

Changes

  • Byte-fair per-terminal delivery queue (crates/freshell-ws/src/terminal_delivery_queue.rs): three service classes (focused / visible / background) receive approximately 8:3:1 BYTES while all are continuously backlogged; empty classes accrue no credit; lanes within a class share bytes equally; per-terminal FIFO and generation-scoped gap coalescing are preserved; terminal.exit stays sequenced behind final output and is never evicted (a connection whose sequenced controls alone exceed the byte+metadata budget is closed with 4008 rather than growing without bound).
  • Connection-local presentation interest (terminal_interest.rs): additive terminalInterestV1 capability, negotiated on hello/ready; clients send transient full terminal.interest snapshots (monotonic revision, never persisted, never queued across disconnects). Malformed or cross-field-invalid snapshots are rejected with INVALID_MESSAGE and never replace the last accepted state; stale/equal revisions are no-ops. Interest changes scheduling of not-yet-sent bytes only — it never attaches, detaches, resizes, spawns, or kills; per-connection, so one viewer never affects another.
  • Fallback for un-negotiated clients: the existing terminal.attach.priority field seeds a terminal's class until the first accepted snapshot (attach_priority_works_for_clients_without_interest_capability pins it); detach drops both queued delivery and the fallback entry (discard_terminal_delivery), attach supersession still discards an old generation's queued frames.

Porting notes (draft handoff integration)

The handoff's connection_writer.rs reference was authored against the ORIGINAL Part-1 candidate. This PR instead ports the scheduling/interest design onto the writer as reviewed and landed in #705, retaining all post-review fixes: join-error reconciliation in teardown (both arms), tick-cycle keepalive (no wall-clock jitter), stray-pong consumption, control-lane byte budget with admit-one-when-empty, stamp-gated leapfrog fairness (CONTROL_STREAK_LIMIT keeps an always-saturated control lane from starving strictly-older output while never reordering a prelude behind its replay), and the drained (never dropped) interactive-create worker semantics. output_frame_meta/DEFAULT_TERMINAL_CLIENT_QUEUE_MAX_BYTES stay shared in freshell-terminal; the now-superseded OutputQueue machinery (and its superseded scheduling test) is removed.

Draft defect found and fixed during porting: the authored update_priorities did not carry normalized service credit across a lane's class migration, so demoting a long-served lane made its new class falsely "underserved" and the demoted lane would beat the freshly-focused lane on the next lease — the opposite of the feature's purpose. terminal_delivery_queue now converts the origin class's weighted service into the destination class's units on migration; the authored test focus_does_not_cancel_the_inflight_frame_or_lose_following_bytes (and reprioritization tests) prove promotion, while the steady-state ratio tests prove the 8:3:1 weighting is unchanged. Admission-time serialization (payloads stored wire-ready) replaced the lease-time re-serialization — required since byte cost and class fairness are admission-time properties — so each frame is now serialized exactly once.

Node server: terminal.interest is accept-and-ignore there (Rust-only feature; it never advertises the capability), mirroring the existing auto-resume precedent.

Fresh-eyes review history

Two rounds of independent review (Claude). Round 1 found three majors — a weight-scaled service-carry starved demoted lanes (replaced by the correct direct watermark lift + history regression test), the attach-priority fallback map could close a long-lived socket at the 1024-entry cap (now snapshot-skip + exit/detach prune + warn-and-skip instead of 4008), and missing client tests (now 4 ws-client + 5 mounted-reporter + 1 publisher cases). Round 1 minors also fixed: zero-weight sequenced exits restored (legacy parity), dead run_with_interest wrapper folded, handshake capability flows through the builder as a parameter, selector refusal is debug-logged, inventory test renamed, stale scheduling docs corrected. Round 2 PASSED with minors/nits only; residual disclosed items are noted here:

  • The queue's metadata/count cap closes with 4008 where the byte cap would have evicted, only when TERMINAL_CLIENT_QUEUE_MAX_BYTES is raised past roughly 42 MiB (default 32 MiB is unreachable); no deployment sets the override today.
  • Playwright-level e2e proving browser publishing over a real socket is a justified follow-up (unit reporter/publisher/ws-client + native wire suites cover the behavior; the Playwright continuity lane runs the real binary).
  • The dispatch-time interest arm splits attach/detach pre-handling from their handlers (kept for the dims guard; bounded and warned).

Verification

  • cargo fmt --check clean; clippy on freshell-ws/freshell-terminal/freshell-protocol with -D warnings clean.
  • 1038 tests pass across the three crates (--no-fail-fast, all targets, host), including the new delivery-queue unit tests (18), interest unit tests, four writer-level interest/reprioritization regressions, and the new real-WebSocket terminal_interest_wire.rs (capability negotiated only on opt-in, accepted snapshot never spawns/attaches, unnegotiated rejection, malformed/stale rejection without disconnect). The pre-existing term09_output_queue wire suite (real sockets, real PTYs, slow reader) passes unchanged.
  • npm run contract:generate refreshed (37 client→server types; the protocol crate's inventory drift guard was updated to 37/97).
  • Client: npm run typecheck clean; focused terminal-interest Vitest suite green; coordinated npm run check green.
  • pane_reconcile_freshagent's dead_session WARN-count assertion is a pre-existing ambient flake: reproduced on a pristine control worktree at the current merge base (cd6a15e), unrelated to this branch.

Explicitly out of scope

Constrained-link browser benchmarks (the architectural expectation is byte-fair scheduling; measured A/B wins are a follow-up), foreground/background interplay with future bandwidth fractions, and the self-hosted deployment (no server restart performed).

…heduling

Adds an additive terminalInterestV1 capability. After the server echoes it on
ready, the browser sends transient full terminal.interest snapshots (focused +
visible terminal IDs, socket-owned monotonic revision; invalid snapshots are
rejected without replacing state; stale revisions are no-ops). The connection
writer's output queue becomes a byte-fair per-terminal delivery queue with
three service classes — focused/visible/background, ~8:3:1 bytes while all are
continuously backlogged — with per-terminal FIFO, generation-scoped gap
coalescing, and sequenced terminal.exit retained (a connection whose sequenced
controls alone exceed the cap closes with 4008). Presentation interest only
reorders not-yet-sent bytes: it never attaches, resizes, spawns, or kills,
and one viewer never affects another. terminal.attach.priority remains the
pre-snapshot fallback for un-negotiated clients; detach drops queued delivery
and the fallback entry.

Ported onto the reviewed #705 writer rather than replacing it: keeps the
join-error teardown reconciliation, tick-cycle keepalive, control-lane budget
with admit-one-when-empty, and stamp-gated leapfrog fairness. The delivery
queue additionally carries admission stamps (front_stamp) so the landed
control-vs-output arbitration survives the scheduling change. Frames are
serialized exactly once at admission (byte cost is an admission-time property
of fair scheduling), superseding the typed-message lease-time serialization;
the now-unused freshell-terminal OutputQueue machinery is removed, keeping
only shared output-frame identity extraction and the default cap.

Draft defect fixed during porting: update_priorities did not carry normalized
service credit across a lane's class migration, so demoting a long-served lane
made the destination class falsely underserved and the demoted lane would beat
the freshly-focused lane — the migration now converts the origin class's
weighted service into the destination's units (pinned by the reprioritization
tests while the steady-state ratio tests still pin 8:3:1).

Client: TerminalInterestReporter (mounted once in the workspace) publishes
coalesced snapshots off selected tab/active pane/zoom/visibility; the revision
lives on WsClient so remounts never rewind it; snapshots are never queued
across disconnects. ws mocks in 22 suites gained the new no-op method, and the
hello-capabilities shape test includes terminalInterestV1. The Node server
accepts-and-ignores terminal.interest (never advertises : Rust-only), matching
the auto-resume precedent. Protocol inventory frozen at 37 client/60 server.

Rust: 1037 tests pass across freshell-ws/-terminal/-protocol (all targets,
--no-fail-fast) including 17 delivery-queue units, interest units, four
writer-level reprioritization regressions, and the terminal_interest_wire
real-socket suite; term09 slow-reader wire suite unchanged and green. npm run
check green on the coordinated gate. Note: pane_reconcile_freshagent's tracing
assertion flaked once across three full-suite runs (passes isolated and on
file rerun; same deferred class noted pre-existing in that suite).
…tests

- update_priorities: drop the weight-SCALED service carry. class_served is one
  normalized virtual clock across classes, so the direct watermark lift is the
  correct carry; the scaled version starved a demoted lane for a period
  proportional to the connection's entire prior service. New regression test:
  demotion_after_history_neither_steals_nor_starves pins both the promotion
  window (demoted lane arrives within one 8:1 window) and the steady ratio
  (exactly 3 of 27 saturated pops) with 1000 frames of real history.
- InterestState.attach: stop storing fallback entries once a snapshot is
  authoritative (the map is never consulted then), prune on detach and on
  terminal.exit admission, and never kill the connection at the cap: skip the
  entry (Visible default = pre-feature behavior) with a
  ws.interest.fallback_cap_reached warn instead of a 4008 close. Removes the
  NX-attempt reconnect-storm path reviewers found.
- Sequenced terminal.exit pushes are zero-weight again (legacy parity): an
  exit can no longer force eviction or trip OutputCapacityExceeded; they stay
  count-bounded by the independent metadata limit. The in-flight reservation
  (cap minus leased frame) is deliberate admission-accounting, now documented.
- run_with_interest folds into run() (the legacy entry point had no callers);
  terminal_interest_v1 flows through build_handshake_with_capabilities as a
  parameter like the sibling capabilities instead of a get_or_insert patch.
- terminal-interest: a refused selector read is now debug-logged instead of
  silently parked (publisher keeps the last accepted snapshot); pinned by a
  new publisher test.
- inventory drift-guard test renamed to combined_surface_is_97; the
  unreachable post-eviction MetadataLimit check is documented as an invariant.
- Client tests for the previously uncovered behavior: 4 WsClient
  sendTerminalInterest cases (refusal before ready, refusal without the
  negotiated capability, monotonic socket-owned revisions, no queueing across
  disconnects + revision reset on the new socket) and 5 mounted
  TerminalInterestReporter cases (mount publish, active-pane/zoom republish,
  eager reflush on ready reconnect, immediate hidden report on
  visibilitychange, workspace-visibility gating). Note: this suite has no
  global RTL cleanup — the reporter tests unmount explicitly to avoid leaking
  visibilitychange listeners across cases.

Note: pane_reconcile_freshagent's dead_session WARN-count assertion flakes on
pristine main too (1/5 in a control worktree at cd6a15e; unrelated to this
branch). Everything else passes: freshell-ws/-terminal/-protocol full suites,
fmt, clippy -D warnings, typecheck.
- backpressure.rs: TERM-07 attach priority IS now carried (attach.priority as
  the pre-snapshot fallback, terminalInterestV1 snapshots after); the old
  'not implemented' paragraph was stale.
- output_queue.rs doc: exits are zero-weight + count-bounded since the
  preceding commit; the 4008 path requires exhausting evictable output while
  over cap.
- connection_writer arbitration comment: sequenced exits keep their admission
  stamp and may leapfrog a control streak (admission order preserved).
- term09_output_queue.rs doc: references the delivered delivery queue, not
  the removed OutputQueue name.
@danshapiro
danshapiro merged commit 3c52b2c into main Sep 5, 2026
3 checks passed
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