Skip to content

feat: hot-swap upgrade — replace the binary while every tab stays live - #23

Open
williamdes wants to merge 2 commits into
mainfrom
claude/hot-swap-tab-atelier-5myscf
Open

feat: hot-swap upgrade — replace the binary while every tab stays live#23
williamdes wants to merge 2 commits into
mainfrom
claude/hot-swap-tab-atelier-5myscf

Conversation

@williamdes

Copy link
Copy Markdown
Member

Add src/hotswap.rs: on 'tab-atelier upgrade' / POST /upgrade the running
process re-execs the binary installed at its own path, handing each
tab's PTY master fd (CLOEXEC cleared) plus a pid-validated JSON manifest
across the exec. Because exec keeps the pid, tab shells remain our
children — process groups, controlling TTYs, cgroups, and nftables
rules are untouched, so agents and builds inside the tabs never notice.

The new image adopts the fds at boot (AdoptedPty mirrors alacritty's
Unix Pty: same poller tokens, SIGCHLD pipe, waitpid exit detection)
instead of forking shells; grid contents restore through the existing
saved-output replay and the carried raw ring bytes re-seed viewer
scrollback. PTY readers freeze during the handoff so unread bytes wait
in the kernel and are parsed by the new binary — nothing is lost.

Adopted tabs skip exactly the work that assumes a fresh shell: agent
auto-resume (would double-launch the still-running claude), the GUI
net-off bubblewrap respawn (still jailed), the headless nftables
teardown/re-apply (would blip enforcement; only the gating DNS resolver
is respawned), the cgroup stale reap, and the agent reaper's provenance
record (removed at swap so the new boot can't SIGKILL the inherited
fleet). A shell that dies mid-swap falls back to a normal fresh fork;
a failed exec rolls back fully and the old binary keeps running.

Trigger surface: POST /upgrade (master token, 409 when no binary at the
re-exec path, 501 on Windows), 'upgrade' subcommand on both binaries,
docs/hot-swap.md, openapi.yaml entry.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01LTkVxFVGsaywyG4iJyTyvc

@williamdes
williamdes force-pushed the claude/hot-swap-tab-atelier-5myscf branch from 755f5f7 to 82e510e Compare July 19, 2026 00:00
@codecov

codecov Bot commented Jul 19, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 55.38194% with 257 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.08%. Comparing base (581bd2d) to head (9e26330).
⚠️ Report is 2 commits behind head on main.

Files with missing lines Patch % Lines
src/hotswap.rs 66.27% 145 Missing ⚠️
src/app.rs 0.00% 46 Missing ⚠️
src/terminal.rs 45.09% 28 Missing ⚠️
src/cli/upgrade.rs 0.00% 24 Missing ⚠️
src/api/upgrade.rs 0.00% 12 Missing ⚠️
src/cli/dispatch.rs 66.66% 1 Missing ⚠️
src/pty_ring.rs 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main      #23      +/-   ##
==========================================
+ Coverage   52.47%   59.08%   +6.60%     
==========================================
  Files          92       95       +3     
  Lines       28268    29440    +1172     
==========================================
+ Hits        14835    17396    +2561     
+ Misses      13433    12044    -1389     
Flag Coverage Δ
rust 59.08% <55.38%> (+6.60%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@williamdes
williamdes force-pushed the claude/hot-swap-tab-atelier-5myscf branch from 82e510e to da6699f Compare July 19, 2026 14:27
@williamdes
williamdes force-pushed the claude/hot-swap-tab-atelier-5myscf branch 4 times, most recently from 893e424 to ee38710 Compare July 27, 2026 16:53
@williamdes
williamdes force-pushed the claude/hot-swap-tab-atelier-5myscf branch 14 times, most recently from 6ebd05c to bf12052 Compare September 2, 2026 15:41
@williamdes
williamdes force-pushed the claude/hot-swap-tab-atelier-5myscf branch 2 times, most recently from 9749627 to 323e2f3 Compare September 3, 2026 06:34
Add src/hotswap.rs: on 'tab-atelier upgrade' / POST /upgrade the running
process re-execs the binary installed at its own path, handing each
tab's PTY master fd (CLOEXEC cleared) plus a pid-validated JSON manifest
across the exec. Because exec keeps the pid, tab shells remain our
children — process groups, controlling TTYs, cgroups, and nftables
rules are untouched, so agents and builds inside the tabs never notice.

The new image adopts the fds at boot (AdoptedPty mirrors alacritty's
Unix Pty: same poller tokens, SIGCHLD pipe, waitpid exit detection)
instead of forking shells; grid contents restore through the existing
saved-output replay and the carried raw ring bytes re-seed viewer
scrollback. PTY readers freeze during the handoff so unread bytes wait
in the kernel and are parsed by the new binary — nothing is lost.

Adopted tabs skip exactly the work that assumes a fresh shell: agent
auto-resume (would double-launch the still-running claude), the GUI
net-off bubblewrap respawn (still jailed), the headless nftables
teardown/re-apply (would blip enforcement; only the gating DNS resolver
is respawned), the cgroup stale reap, and the agent reaper's provenance
record (removed at swap so the new boot can't SIGKILL the inherited
fleet). A shell that dies mid-swap falls back to a normal fresh fork;
a failed exec rolls back fully and the old binary keeps running.

Trigger surface: POST /upgrade (master token, 409 when no binary at the
re-exec path, 501 on Windows), 'upgrade' subcommand on both binaries,
docs/hot-swap.md, openapi.yaml entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTkVxFVGsaywyG4iJyTyvc
@williamdes
williamdes force-pushed the claude/hot-swap-tab-atelier-5myscf branch from 323e2f3 to 145b078 Compare September 3, 2026 06:37

Copy link
Copy Markdown
Member Author

CI note: Build & test & lint is failing on this branch, but the failures are not from this PR.

The three failing tests were added on main by 8128a04 (“test(cli): drive the CLI verbs against a real in-process API server”), and main’s own push run on that commit fails identically (run 33722747048):

  • cli::dispatch::tests::dispatch_routes_each_command_to_its_verb — expects net-off/net-on to queue 2 net changes, but net-off is refused because bubblewrap isn’t installed on the CI runner (“bubblewrap (bwrap) is not installed on the daemon host”), so only 1 is queued.
  • cli::share_link::tests::net_toggles_and_allowlists_reach_the_daemon — same bwrap dependency, plus net-allow needs the headless nftables path.
  • cli::share_link::tests::ssh_agent_and_default_limits_are_queuedssh-agent refuses under the GUI edition (“per-tab ssh-agent requires the headless daemon”), and the default-features test suite runs the GUI edition.

All three pass under --no-default-features --features headless,… locally (the coverage numbers in 8128a04’s message were measured on the headless lib). Likely fixes on main: gate these tests/asserts to the headless feature, or install bubblewrap in the workflow. I’ll rebase this branch as soon as main is green again; no fix exists on main yet to port.


Generated by Claude Code

The three tests added with the in-process API harness assumed the
headless edition on a host with bubblewrap — exactly what CI's GUI
lib run is not, so main went red:

- net-off is refused (412) without bwrap on the daemon host; assert
  the refusal there and the queued jail only when bwrap exists.
- net-allow answers 501 on the GUI (nftables enforcement needs
  CAP_NET_ADMIN the desktop build does not have): assert the verb
  FAILS there, and keep the queue assertions headless-only.
- ssh-agent is the same 501-on-GUI story; the queue checks move under
  cfg(not(feature = "gui")) and the GUI asserts the refusal.

Verified green in all four combinations: GUI/headless lib tests, each
with and without bwrap on PATH.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LTkVxFVGsaywyG4iJyTyvc

Copy link
Copy Markdown
Member Author

Update: opened #50 with the fix for the three edition/host-gated CLI tests, and cherry-picked the same commit onto this branch (9e26330) so CI here goes green without waiting on the merge. Once #50 (or an equivalent fix) lands on main, the next rebase will drop the duplicate commit automatically.


Generated by Claude Code

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.

2 participants