Skip to content

fix(server): transfer handoff descriptors in batches - #3411

Open
kataokatsuki wants to merge 1 commit into
herdrdev:masterfrom
kataokatsuki:issue/3393-handoff-descriptor-batches
Open

fix(server): transfer handoff descriptors in batches#3411
kataokatsuki wants to merge 1 commit into
herdrdev:masterfrom
kataokatsuki:issue/3393-handoff-descriptor-batches

Conversation

@kataokatsuki

@kataokatsuki kataokatsuki commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Problem

herdr server live-handoff refuses a session with 65 or more panes:

live handoff supports at most 64 panes in one update; close panes or
restart herdr normally

The session then stays on the old build while the CLI moves ahead, and the
only supported way out is a normal restart, which ends the shell and agent
processes in every pane.

#3393 reports the same limit from herdr update --handoff, where the rejected
handoff additionally leaves the freshly installed CLI protocol-locked out of
the server that is still running. This change covers only the third option
listed there, chunking the transfer; the install ordering and the protocol
check on recovery commands are untouched, so it does not close that issue on
its own.

Cause

send_fds packs every pane's pty master descriptor into a single
SCM_RIGHTS control message and calls sendmsg once. recv_fds mirrors
that with one recvmsg. A single control message cannot carry an unbounded
number of descriptors, so MAX_FDS_PER_HANDOFF = 64 guards the send, once in
send_fds_and_wait_restored and once in perform_live_handoff before any
work starts.

Fix

  • send the descriptors in batches of FDS_PER_MESSAGE (64)
  • drop both pane-count guards
  • the receive side asks for min(remaining, 64) descriptors per recvmsg
    and accumulates across calls until expected have arrived, iterates every
    cmsghdr with CMSG_NXTHDR and bounds each SCM_RIGHTS payload by
    msg_controllen before reading it, rejects a batch that carries more
    descriptors than it asked for, treats MSG_CTRUNC as unrecoverable because
    the kernel has already closed the descriptors that did not fit, and closes
    the descriptors it already holds on any failure

A session of 64 panes or fewer still produces exactly one batch, so its wire
representation is unchanged and old and new implementations stay compatible
across the range that works today. A new sender handing more than 64 panes to
an old receiver (a downgrade) still fails, but that pane count is already
unsupported. HANDOFF_VERSION stays at 1; raising it would refuse even the
64-and-under handoffs that currently succeed.

The sending side of a handoff is always the previously running binary, so the
first update onto a build that carries this change is still refused by the old
server's guard; batching takes effect from the next handoff after that.

64 is kept as the batch width rather than raised: a single SCM_RIGHTS
message accepts at most 253 descriptors on Linux and 254 on macOS, measured
with a socketpair program, and 64 leaves room under both.

I'm happy to bump HANDOFF_VERSION or widen the batch if you'd rather.

Verification

Measured on base e7d8220.

  • macOS, end-to-end against a real server with an isolated config, runtime
    and socket path:

    build panes server live-handoff panes after
    origin/master 65 fails: at most 64 panes
    origin/master 71 fails: at most 64 panes
    this branch 65 completes 65
    this branch 66 completes 66
    this branch 71 completes 71
    this branch 131 completes 131

    131 panes splits into three batches (64 + 64 + 3), so this covers more than
    the two-batch case. The script creates one pane per tab over the socket API
    and then runs herdr server live-handoff --import-exe against the same
    binary; "panes after" is herdr pane list on the replacement server.

  • Linux aarch64: cargo nextest run --locked -E 'binary(live_handoff)'; the
    new test passes, and 5/5 when repeated alongside
    live_server_holds_one_pty_master_fd_per_pane.

New test live_handoff_carries_more_panes_than_one_scm_rights_message builds
a 70-pane session, hands off, and asserts the replacement server holds 70 pty
master descriptors and lists 70 panes.

refs #3393

@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: a7d92a84-5c5f-4c3f-b3ee-69f96b84158b

📥 Commits

Reviewing files that changed from the base of the PR and between 359f362 and 0ed1260.

📒 Files selected for processing (1)
  • tests/live_handoff.rs

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

The handoff protocol now sends file descriptors in batches of 64. The receiver collects multiple SCM_RIGHTS messages and closes received descriptors when validation fails. Live handoff no longer rejects more than 64 panes. Tests verify session preservation after pane exit and transfer of 70 pane PTY master descriptors.

Suggested reviewers: ogulcancelik

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 0ed12

Live handoff now supports pane counts beyond a single SCM_RIGHTS message while retaining the existing representation for smaller handoffs. Multi-batch transfer and pane preservation are covered by the added live-handoff test, with no remaining merge-blocking risk identified.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 2 files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes the main change: transferring handoff descriptors in batches.
Description check ✅ Passed The description directly explains the descriptor limit, batching fix, receive-side validation, compatibility, tests, and verification results.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kangal-bot kangal-bot added the ai-review Trigger automated AI reviews for pull requests admitted by the PR gate label Aug 30, 2026
@kataokatsuki kataokatsuki reopened this Sep 1, 2026
@kangal-bot

Copy link
Copy Markdown
Collaborator

@coderabbitai review
@greptileai

@kataokatsuki

kataokatsuki commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

Reopening so the Greptile review runs; this PR predates 4dd9aa5. The head commit is unchanged at 122de79.

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== Changed files ==='
git diff --name-only HEAD^ HEAD

printf '%s\n' '=== PR diff: handoff ==='
git diff HEAD^ HEAD -- src/server/handoff.rs

printf '%s\n' '=== PR diff: headless and test ==='
git diff HEAD^ HEAD -- src/server/headless.rs tests/live_handoff.rs

printf '%s\n' '=== Source outlines ==='
ast-grep outline src/server/handoff.rs --items all
ast-grep outline src/server/headless.rs --items all
ast-grep outline tests/live_handoff.rs --items all

printf '%s\n' '=== Protocol call sites and definitions ==='
rg -n -C 4 -P '\b(send_fds|recv_fds|send_fds_and_wait_restored|recv_fds_and_send_restored|send_fd_batch|recv_fd_batch|close_raw_fds)\s*\(' \
  src tests

Length of output: 40304


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== Handoff setup, manifest exchange, and import ==='
nl -ba src/server/handoff.rs | sed -n '130,320p'

printf '%s\n' '=== Descriptor send and receive implementation ==='
nl -ba src/server/handoff.rs | sed -n '370,530p'

printf '%s\n' '=== Live-handoff sender ownership and rollback ==='
nl -ba src/server/headless.rs | sed -n '1200,1415p'

printf '%s\n' '=== Existing live-handoff failure tests ==='
nl -ba tests/live_handoff.rs | sed -n '1720,1975p'

printf '%s\n' '=== Direct tests of descriptor transfer helpers ==='
rg -n -C 5 -P '\b(send_fds|recv_fds|SCM_RIGHTS|MSG_CTRUNC|handoff fd)\b' src/server/handoff.rs tests

Length of output: 1066


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== Handoff setup, manifest exchange, and import ==='
sed -n '130,320p' src/server/handoff.rs

printf '%s\n' '=== Descriptor send and receive implementation ==='
sed -n '370,530p' src/server/handoff.rs

printf '%s\n' '=== Live-handoff sender ownership and rollback ==='
sed -n '1200,1415p' src/server/headless.rs

printf '%s\n' '=== Existing live-handoff failure tests ==='
sed -n '1720,1975p' tests/live_handoff.rs

printf '%s\n' '=== Direct tests of descriptor transfer helpers ==='
rg -n -C 5 -P '\b(send_fds|recv_fds|SCM_RIGHTS|MSG_CTRUNC|handoff fd)\b' src/server/handoff.rs tests

Length of output: 34819


@kangal-bot: I will review pull request #3411.

⚠️ Action not completed

Already reviewed the last commit. Use @coderabbitai full review to rerun a review of the entire changeset.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@greptile-apps

greptile-apps Bot commented Sep 1, 2026

Copy link
Copy Markdown

Greptile Summary

The PR removes the 64-pane live-handoff limit by transferring Unix PTY descriptors through multiple bounded SCM_RIGHTS messages and accumulating them on the replacement server.

  • Splits descriptor sends into batches of 64.
  • Receives and validates batches until the manifest’s expected descriptor count is reached.
  • Closes accumulated descriptors on malformed, truncated, short, or failed transfers.
  • Removes the headless-server pane-count preflight rejection.
  • Adds an end-to-end 70-pane handoff test.

Confidence Score: 5/5

The PR appears safe to merge, with no concrete correctness or security failures identified in the changed handoff path.

Descriptor ordering remains aligned with manifest ordering, receive-side failures close acquired handles, and pre-commit incompatibilities retain the existing rollback behavior.

Important Files Changed

Filename Overview
src/server/handoff.rs Replaces the single-message descriptor transfer with bounded batching, multi-message receipt, validation, and explicit error cleanup.
src/server/headless.rs Removes the 64-pane preflight guard so the handoff layer can transfer arbitrarily many pane descriptors.
tests/live_handoff.rs Adds an end-to-end 70-pane test that verifies descriptor ownership and pane topology after a multi-batch handoff.

Sequence Diagram

sequenceDiagram
    participant O as Old server
    participant N as Replacement server
    O->>N: Manifest with expected pane count
    loop Until every PTY descriptor is transferred
        O->>N: 1 byte + SCM_RIGHTS (up to 64 FDs)
        N->>N: Validate and accumulate batch
    end
    N->>N: Restore pane runtimes in manifest order
    N-->>O: restored
    O->>N: commit
    N-->>O: ownership acknowledged
Loading

Reviews (1): Last reviewed commit: "fix(server): transfer handoff descriptor..." | Re-trigger Greptile

@kataokatsuki
kataokatsuki force-pushed the issue/3393-handoff-descriptor-batches branch from 122de79 to 359f362 Compare September 6, 2026 06:53
@kataokatsuki

kataokatsuki commented Sep 6, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto 9e9bc8a to resolve the conflict from the headless.rs split;
the pane-count guard is now removed in src/server/headless/lifecycle.rs.

@coderabbitai

coderabbitai Bot commented Sep 6, 2026

Copy link
Copy Markdown

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

Live handoff refused any session with more than 64 panes. The pane count
was checked twice against MAX_FDS_PER_HANDOFF, and the transfer itself put
every pane's pty master into one SCM_RIGHTS control message, so the guard
was the only thing keeping the send inside the kernel's per-message limit.
A session past the limit could only be updated by closing panes or by a
normal restart, which ends every pane process.

Send the descriptors in batches of 64 instead and drop both guards. The
receiving side accumulates across recvmsg calls until the expected count
arrives, bounds every SCM_RIGHTS payload it reads by the control bytes the
kernel returned, rejects a batch that carries more descriptors than it
asked for, and closes the descriptors it already holds on any failure. A
session of 64 panes or fewer still produces one batch, so the bytes on the
wire are unchanged and HANDOFF_VERSION stays at 1.

refs herdrdev#3393
@kataokatsuki
kataokatsuki force-pushed the issue/3393-handoff-descriptor-batches branch from 359f362 to 0ed1260 Compare September 9, 2026 12:34
@kataokatsuki

Copy link
Copy Markdown
Contributor Author

Rebased onto e7d8220; the added test moved below the one that #3418 introduced in tests/live_handoff.rs. The earlier macOS failure was the flake in #3685.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-review Trigger automated AI reviews for pull requests admitted by the PR gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants