a73x

449f80eb

refactor: one seating tail for both attach arms

a73x   2026-08-29 10:01

Commit message
refactor: one seating tail for both attach arms

onAttach and the observer .attach arm spelled the same eight lines after
their own refusals — count, bump, size, record, modes, resync, cmd state.
seatClient owns them, so a ninth step cannot be added to the client arm
and forgotten on the socket client's first attach, which is the observer
one.

Pinned by the attach tests in server_test_deliver/server_test_session and
by e2e_02's attach leg; the wall tests hold `attaches=` still across a zoom.

src/server/server.zig
Old New
@@ -2036,9 +2036,6 @@ pub const Server = struct {
2036 _ = self.queueFrame(i, .exit_status, &.{1}); 2036 _ = self.queueFrame(i, .exit_status, &.{1});
2037 return; 2037 return;
2038 }; 2038 };
2039 // Past the refusal, so the counter never grows for an
2040 // attach that attached nobody. See upgrade.Counters.attaches.
2041 self.stats.attaches += 1;
2042 if (self.clients[i]) |*c| { 2039 if (self.clients[i]) |*c| {
2043 // An await's since_seq is a watermark in the OLD 2040 // An await's since_seq is a watermark in the OLD
2044 // session's tracker, and seq series are per-session — 2041 // session's tracker, and seq series are per-session —
@@ -2052,12 +2049,20 @@ pub const Server = struct {
2052 if (c.session != si) c.await_state = null; 2049 if (c.session != si) c.await_state = null;
2053 c.session = si; 2050 c.session = si;
2054 } 2051 }
2052 self.seatClient(i, si, req);
2053 }
2054
2055 /// The seating both attach arms end on, past every refusal so `attaches`
2056 /// never grows for an attach that seated nobody. Size is recorded only
2057 /// when the grid really went there (a refused one leaves the slot 0x0),
2058 /// modes go ahead of the state they describe, and only a size change
2059 /// repaints anyone but the joiner.
2060 fn seatClient(self: *Server, i: usize, si: usize, req: proto.AttachReq) void {
2061 self.stats.attaches += 1;
2055 self.bumpActivity(i); 2062 self.bumpActivity(i);
2056 const size_changed = (req.cols != self.colsNow(si) or req.rows != self.rowsNow(si)); 2063 const size_changed = (req.cols != self.colsNow(si) or req.rows != self.rowsNow(si));
2057 const applied = self.applySize(si, req.cols, req.rows); 2064 const applied = self.applySize(si, req.cols, req.rows);
2058 if (applied) self.recordSize(si, i); 2065 if (applied) self.recordSize(si, i);
2059 // Ahead of the state it describes, so a client can never be
2060 // holding grid content it has no mode for.
2061 self.sendPtyModeTo(si, i); 2066 self.sendPtyModeTo(si, i);
2062 self.sendResync(si, i, req.have_seq, req.have_epoch, size_changed and applied); 2067 self.sendResync(si, i, req.have_seq, req.have_epoch, size_changed and applied);
2063 self.sendCmdStateTo(si, i); 2068 self.sendCmdStateTo(si, i);
@@ -2506,33 +2511,11 @@ pub const Server = struct {
2506 const si = self.sessions.resolve(self, sz.name, sz.cols, sz.rows) orelse { 2511 const si = self.sessions.resolve(self, sz.name, sz.cols, sz.rows) orelse {
2507 return self.refuseObserver(i); 2512 return self.refuseObserver(i);
2508 }; 2513 };
2509 // Past both refusals above, so the counter only ever grows
2510 // for an attach that seated somebody. See upgrade.Counters.attaches.
2511 self.stats.attaches += 1;
2512 // Promote: the buffer moves with the fd. 2514 // Promote: the buffer moves with the fd.
2513 const moved = self.observers[i].?.inbound; 2515 const moved = self.observers[i].?.inbound;
2514 self.observers[i] = null; 2516 self.observers[i] = null;
2515 self.clients[slot] = .{ .sink = .{ .socket = fd }, .session = si, .inbound = moved }; 2517 self.clients[slot] = .{ .sink = .{ .socket = fd }, .session = si, .inbound = moved };
2516 // The other half of the .attach arm's bump: a socket 2518 self.seatClient(slot, si, sz);
2517 // client's FIRST attach lands here, not in handleFrame, so
2518 // leaving it out would seat every fresh CLI client at 0 and
2519 // order them all identically until somebody typed.
2520 self.bumpActivity(slot);
2521 const size_changed = (sz.cols != self.colsNow(si) or sz.rows != self.rowsNow(si));
2522 const applied = self.applySize(si, sz.cols, sz.rows);
2523 // The slot's size is what latest-wins re-reads when this
2524 // client types, so it is set here and kept current by the
2525 // .resize/.attach arms — but only when the grid really went
2526 // there. A refused attach must leave the slot at 0x0.
2527 if (applied) self.recordSize(si, slot);
2528 self.sendPtyModeTo(si, slot);
2529 // Latest wins: a size change broadcasts, repainting every
2530 // client at the new attacher's size. A same-size join is
2531 // the joiner's business alone — see sendResync. So is a
2532 // refused one: the size differs but the grid never moved,
2533 // so there is nothing to repaint anyone else for.
2534 self.sendResync(si, slot, sz.have_seq, sz.have_epoch, size_changed and applied);
2535 self.sendCmdStateTo(si, slot);
2536 // Frames the same write carried behind the attach (a wall 2519 // Frames the same write carried behind the attach (a wall
2537 // tile sends agent_offer on its heels) are handled now, as 2520 // tile sends agent_offer on its heels) are handled now, as
2538 // the client they were addressed to. 2521 // the client they were addressed to.