fix(acpmux): resume peers across service restart
A daemon update currently drops peer and web sockets without a restart Close frame, so peers treat the handoff as a transport failure and wait through normal reconnect backoff. This change closes admitted WebSocket clients with Close 1012 and makes the next peer connection attempt immediate. A failed attempt resumes the existing failure backoff, and mirrored session rows remain available during the restart.
Leo (2026-10-28): "Are we covering the edge cases for when, you know, something isn't going right, if there's recovery, whatever, stuff like that ... these are all things that are under our umbrella that we need to do, and we need to do really, really f***ing well."
Leo (2026-10-21): "ACCEPT IT BREAKING AT SOME POINT BUT IT RESUMES WHATEVER IT INTERRUPTED. END OF STORY FOR THE DAEMON UPDATE."
TCP and tailnet listeners close admission before sending restart frames. Shutdown cancels blocked writes, bounds each Close write to one second, and bounds aggregate listener draining to two seconds before tailnet teardown. Listener tasks and their children are cleaned up when draining ends or times out. No new polling timer is introduced.
daemon restart, shutdown RPC {restart:true}, and the detached updater's daemon shutdown --keep-agents --restart request restart signaling. The updater falls back for older CLIs only when clap rejects a new flag. Supervisors use SIGTERM for both updates and permanent stops, so SIGTERM also sends 1012: a permanently stopped service causes one immediate retry, then normal failure backoff. Ordinary RPC shutdown (including plain --keep-agents), SIGINT, and logout use normal close behavior.
Peer summary JSON reports restarting. The internal restart notice bypasses disconnect grace and preserves rows; the existing typed wire status remains Reconnecting, with no generated or UI protocol change. Authentication, permission checks, agent-host adoption, and updater idle policy are unchanged.
Risk
High: shutdown ordering, WebSocket cleanup, and reconnect timing. Slow readers may exhaust the bounded close budget without receiving the frame; peers then follow the normal failure path. SIGTERM cannot distinguish permanent supervisor stop from restart, so the one-immediate-retry behavior applies to both.
Validation
Final head: 250832756d79ac4124e984394eaaf12d08b4197d, based on 8c5ab0469af82e9003925b97df669561d156df01.
Passed: hosted run 40859938033, exact head above, exit 0. Covers restart library tests, updater compatibility and updater unit tests, public-API peer restart integration, the isolated daemon SIGTERM/restart/ordinary-stop integration, and tailnet restart/blocked-writer tests. Also passes cargo clippy --locked --keep-going -p acpmux -p cmux-tui --all-targets -- -D warnings and cargo clippy --locked -p acpmux --all-targets --features tailnet -- -D warnings.
Behavioral baseline: hosted run 40859204912 checked out unmodified base 8c5ab0469af82e9003925b97df669561d156df01 and copied only the public-API regression test. Compilation passed, then close_1012_reports_restarting_retries_immediately_then_resumes_failure_backoff failed at peer_restart.rs:40: actual reconnecting, expected restarting, with peer closed the connection.
Independent Astra review (gpt-6-astra, high, fast) returned SHIP at this exact head. The final review checked the equivalent drain-condition rewrite, retaining the reviewed credential-fixture correction at 136f99ae58a9a7247618f1ca63e5f2d42449fac7 and full production review at 02932e2c6f14a27f627f79b027e374ed67fd6d9c.
Remaining automatic failures were compared against base by the coordinator and are warnings for this change:
| Check | Failure and base comparison |
|---|---|
| Protocol contract, inventory | Deterministic SDK generator test_ir.IrTests.test_live_raw_v12_layout_and_history_contracts_are_exact, new-pane-right field-map assertion; same failure on base run 40853653971. Fixture digest warnings are noncausal. |
| NEXT checks | No-new-SwiftUI check: existing HomeChatColorsPanel.swift and HomeChatColorsView.swift imports, unchanged from base. |
| Release-notary guard | scripts/whats-new/test_digest.py test_the_repository_highlights_validate: tool-audio-discovery.md (0.0.0-nightly.1.json: entries[0] (tool-audio-discovery): needs a tryIt action/deeplink or a docs link). That highlight came onto base with #27362; this PR touches no docs or What's New files. |
| Guard status | Aggregate failure from the release-notary guard above. |
The introduced clippy collapsible_if finding in daemon drain cleanup was fixed in the final head; both hosted clippy configurations now pass.
All builds and test suites ran on hosted Linux. No live daemon was operated and no local test suite ran. The daemon integration test launches and stops only its own isolated in-memory child.
Changelog
none
Agent session: agent session