Skip to content

cmux-next: local acpmux and cmux-tui move to the bundled build by themselves - #23199

Closed
teamleaderleo wants to merge 19 commits into
feat-cmux-nextfrom
daemon-autoupdate
Closed

teamleaderleo wants to merge 19 commits into
feat-cmux-nextfrom
daemon-autoupdate

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Status at 05367c0: Codex (gpt xhigh) passes this head. The acpmux watchdog sends its signal by the daemon's audit token (proc_signal_with_audittoken, looked up at run time because macOS 14.0 and 14.1 lack it), so a pid handed to another process is never signalled however long the app stalls.

Leo-ask: ask-98502f19a4
Leo-ask: ask-48bbaec1d4
Leo-ask: ask-e8a08a017c

Leo: "OUR SHIT IS HARDLY EVER GOING TO GO IDLE. IT JUST WONT. YOU'LL JUST HAVE TO ACCEPT IT BREAKING AT SOME POINT BUT IT RESUMES WHATEVER IT INTERRUPTED. END OF STORY FOR THE DAEMON UPDATE."

So no wait here is open-ended: a stale cmux-tui waits at most 2 minutes for a quiet moment and a stale acpmux with agent hosts at most 10 minutes, then each is handed off between requests. #24026 then restarts a stale local acpmux at once and shows its build in Settings > Updates.

Summary

After a cmux NEXT update (nightly, beta or release), the app's own daemons could stay on the old build. A local acpmux with no agent hosts was kept running indefinitely ("its agents would end"). A cmux-tui found stale after launch waited for the manual Restart button. A daemon that was alive but hung had to be restarted by hand. This PR is the local-app half of keeping every daemon on the bundled build:

  • Stale acpmux. At every lookup (launch and each reconnect), a daemon of another build is handed off once no session is running a turn or waiting on a permission. This now includes daemons with no agent hosts; their chats resume from disk at the next prompt.
    • While it is busy, the app waits on a one-shot _acpmux/watch. A daemon with agent hosts waits at most 10 minutes; the wait races a deadline on that cap, so a long turn that only streams output can't hold it past the cap.
    • Health check: the next daemon must report the bundled build within 5 s. It gets one retry, then the "acpmux did not start" banner shows.
  • Hung acpmux. If 3 status calls miss their deadline across 20 s, a 5 s confirm runs, then SIGTERM. SIGKILL follows after a 10 s exit wait. This happens at most once per pid. Agent hosts survive SIGTERM and the next daemon adopts them.
    • The daemon is known by its pid and start time (proc_pidinfo), checked again right before each signal and only after the kernel exit watch is on the pid. A pid reused by another process counts as the daemon's exit and is never signalled.
  • Stale cmux-tui. QuietDaemonUpdate checks every handshake against the bundled build. The build is read again each time, so an in-place app update is noticed. A stale daemon is handed off with the existing lossless handOffForUpdate once quiet:
    • Quiet means the person has been idle 60 s or the app is in the background, no menu or drag is in progress, no app request is in flight, and the owner has no CLI, MCP, chief-chat or TUI client attached.
    • It waits for that for at most 2 minutes from when the daemon was found stale. After that, only an app request in flight or a menu, drag or resize loop defers it, retrying every 5 s. An attached TUI exits and reattaches; an attached cmux chief chat exits (code 3) and a CLI or MCP call in flight fails, and both can be run again.
    • Health check: the new daemon must report the bundled build within 90 s. It gets one retry, then a "Couldn't restart cmux-tui" toast.
  • Hung cmux-tui owner. After 2 or more stuck server ensure results over 30 s, the owner is replaced (--replace-unresponsive) with no confirm. This happens once per owner process and at most 3 times per launch. An owner answering server.busy is never replaced.

Each behaviour has a default-on Debug Settings toggle: agent.replaceStaleAcpmuxWhenQuiet, agent.restartUnresponsiveAcpmux, cmuxTUI.restartStaleWhenQuiet, cmuxTUI.restartUnresponsive. The rules are written up in plans/cmux-next/server-updates-DESIGN.md (rule 2a/2b and the status table).

Coming next, in separate PRs:

  • acpmux host update with rollback, idle check, arch check and migration of hand-written units;
  • automatic updates of ssh peers and cloud machines to the app's pinned build;
  • a daemon-capabilities file for web-ota compatibility.

Testing

Tests added:

  • CmuxNextAgentPaneTests
    • AcpmuxQuietHandoffTests: a stale daemon without agent hosts, busy then quiet, the agent-host cap, the health check with retry and banner; aBusyDaemonWithAgentHostsIsHandedOffAtTheCapWithNoEvent (the cap fires with no event and no reconnect, and not before).
    • AcpmuxUnresponsiveWatchTests: the miss window, confirm, once per pid, toggle off. AcpmuxUnresponsiveIdentityTests: a reused pid is never signalled; a daemon that exits after its identity check gets no signal (aDaemonThatExitsAfterItsCheckGetsNoSignal); a signal whose token names an earlier process with the pid is refused by the kernel (aSignalForAnotherProcessWithThePidIsRefused).
  • CmuxNextDaemonTests
    • UnresponsiveOwnerWatchTests, IdleOwnerRestartRetryTests.
    • QuietUpdateKeepsTerminalTests: a live cmux-tui in a temp session. The quiet restart waits for a CLI call and the terminal keeps running.
  • CmuxNextAppTests
    • QuietDaemonUpdateTests: covers an old daemon next to an app updated while running, a successor of the wrong build or one that never connects, anAttachedClientBetweenTurnsHoldsTheRestartOnlyUntilTheCap and pastTheCapARequestInFlightStillDefersIt.
    • DaemonAutoRestartTests, plus ServerUpdateNoticeTests.anotherBuildWithNothingMissingIsAPlainNotice.

Observed / Not run

  • CI's first test run found that the quiet cmux-tui restart could never fire. The daemon's own agent-screen detector holds two kindless connections forever, and the idle probe counted each kindless local client as a CLI call. Now a kindless client counts as a call only for its first 60 s (4f7d43f).
  • On cmux16 (macOS 27), swift build --build-tests of the package passes at the new head:
    • QuietUpdateKeepsTerminalTests passed 3/3 and QuietDaemonUpdateTests 5/5 (at 4f7d43f);
    • AcpmuxQuietHandoffTests, AcpmuxQuietHandoffDecisionTests, AcpmuxUnresponsiveWatchTests, AcpmuxUnresponsiveRestartTests and base's AcpmuxStrayDaemonTests passed 3/3.
  • Those runs found three fixture bugs in this PR's own tests (90860f5). CI never finished those suites because its test helper died first.
    • The stand-in daemon's ready line used a ws:// URL, which the launcher refuses.
    • macOS 27 kills a copied /usr/bin/nc, so the copy is now signed ad hoc.
    • The old daemon's exit event can come before the process table shows it gone, so that check now waits (bounded).
  • CI's UnresponsiveOwnerWatchTests, IdleOwnerRestartRetryTests, QuietUpdateKeepsTerminalTests and OwnerRestartWaitsForChatTests passed in the daemon-tests job.
  • At c38a347 on cmux16: the named suites above passed, 49 tests in 8 suites. ClassicUpgradeSimulationTests was not run there: the build guard refused it for low disk, so CI's app-tests job is its run.
  • Not run outside CI: the watchdog commits after c38a347 (f8f10fc: the kqueue exit watch before the identity check; d969314 and 05367c0: the audit-token signal). cmux16 had too little disk for another package build, so CI's package-test lane is their only run. aSignalForAnotherProcessWithThePidIsRefused expects the kernel's refusal, so it would fail on macOS 14.0 or 14.1, where the token call is missing and nothing is sent; no CI runner uses those versions.
  • Never run against a live daemon: no daemon on any of Leo's machines was restarted while developing this.

No capture: there's no visible UI to show. The change is daemon upkeep (handoffs, restarts and watchdogs) behind the Debug toggles.

Changelog

Changed: cmux NEXT moves its local acpmux and cmux-tui daemons to the app's own build after an update, at a quiet moment or after at most a few minutes, and restarts either one if it stops answering

Proof

No UI change beyond the existing toasts and banner; behaviour is covered by the tests above.

Checklist

  • Behavior changes have added or updated tests
  • Localization: the Debug toggle titles (one renamed to "Update a stale cmux-tui automatically") are debug-only. The cmux-tui failure reasons are English inside the localized "Couldn't restart %1$@: %2$@" (follow-up).
  • Reviewed with a subagent before merge

Agent session: Air-Blue

🤖 Generated with Claude Code

@teamleaderleo teamleaderleo added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 21, 2026
@teamleaderleo

teamleaderleo commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator Author

Owned by hb-acpmux on big-red · session

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Review d022ab1: HQ feed card · gallery before/after runs · checks

App of this head (app.zip, kept 1 day)

gh workflow run reload-build.yml -R manaflow-ai/cmux --ref feat-cmux-next -f tag=pr23199 -f ref=daemon-autoupdate -f platform=macos -f backend_mode=local

Passes: CI passes on d022ab1d96.

CI passes on d022ab1d96 (run 39866587787 attempt 1).

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

red: cmux-next checks (god files, concurrency, crash safety, l10n); no finished base run on 84d5d32 to compare; do not merge until green; see lane-rules

Tour of da84a7b3b6 (run): no media. The tour job failed before the tour ran (build or setup); the run log has the output.

@blacksmith-sh

This comment has been minimized.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Bead cx-1idoh created for this PR by the cmux coordinator.

teamleaderleo added a commit that referenced this pull request Oct 21, 2026
…off at a time

From the review of #23199:
- the unresponsive watch restarts each home at most 3 times per launch;
- the quiet wait asks once more after its watch is on, so a turn end
  between the quiet check and the watch is not missed;
- a recheck waits out lookups already under way instead of reusing their
  sessions read, and a start waits for a reconnect's lookup, so two
  handoffs never run at once;
- two QuietDaemonUpdate tests wait for the state they assert on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d26-06d5-7823-8372-148833c4be22&host=cosmic-gray
@cursor

cursor Bot commented Oct 21, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

1 similar comment
@cursor

cursor Bot commented Oct 21, 2026

Copy link
Copy Markdown

Bugbot needs on-demand usage enabled

Bugbot uses usage-based billing for this team and requires on-demand usage to be enabled.

A team admin can enable on-demand usage in the Cursor dashboard.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

codex-review: SHIP at a1432fe

  • Reviewed the current diff, its focused tests, and the affected call paths; no correctness bug found.

teamleaderleo added a commit that referenced this pull request Oct 21, 2026
…off at a time

From the review of #23199:
- the unresponsive watch restarts each home at most 3 times per launch;
- the quiet wait asks once more after its watch is on, so a turn end
  between the quiet check and the watch is not missed;
- a recheck waits out lookups already under way instead of reusing their
  sessions read, and a start waits for a reconnect's lookup, so two
  handoffs never run at once;
- two QuietDaemonUpdate tests wait for the state they assert on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d26-06d5-7823-8372-148833c4be22&host=cosmic-gray
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (independent subagent): SHIP at 020c8bd

Rebase of a1432fe (reviewed SHIP) onto feat-cmux-next. The reviewer checked that all 34 files have the same changed lines as before the rebase. The one conflict, TunableCatalog.all in DebugSettingsService.swift, keeps every base entry and adds DaemonTunables.all once. No base commit adds a caller of an API this PR changes.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next guards / defaults-snapshot is red on 20 of 23 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

teamleaderleo and others added 5 commits October 21, 2026 08:43
…mselves

After an app update (nightly, beta or release) the local daemons no longer stay
on the old build:

- acpmux: a daemon of another build is handed off at a quiet moment at every
  lookup, also one without agent hosts (its chats resume from disk). Busy waits
  on _acpmux/watch; a daemon with agent hosts waits at most 10 minutes. The next
  daemon must report the bundled build within 5 s (one retry, then the banner).
- acpmux that stays alive but stops answering is restarted (3 missed status
  calls over 20 s, confirm, SIGTERM, SIGKILL after 10 s), once per pid; agent
  hosts survive and are adopted.
- cmux-tui: QuietDaemonUpdate checks every handshake against the bundled build
  (read again, so an in-place update is noticed) and hands a stale daemon off
  with handOffForUpdate when the person, the owner and the app are quiet; never
  forced, toast after 30 minutes, 90 s health check with one retry.
- cmux-tui owner that stays stuck for 30 s (2+ stuck ensures) is replaced
  without the confirm, once per owner process; server.busy is never replaced.

Each behind a default-on Debug toggle (agent.replaceStaleAcpmuxWhenQuiet,
agent.restartUnresponsiveAcpmux, cmuxTUI.restartStaleWhenQuiet,
cmuxTUI.restartUnresponsive).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d26-06d5-7823-8372-148833c4be22&host=cosmic-gray
…off at a time

From the review of #23199:
- the unresponsive watch restarts each home at most 3 times per launch;
- the quiet wait asks once more after its watch is on, so a turn end
  between the quiet check and the watch is not missed;
- a recheck waits out lookups already under way instead of reusing their
  sessions read, and a start waits for a reconnect's lookup, so two
  handoffs never run at once;
- two QuietDaemonUpdate tests wait for the state they assert on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d26-06d5-7823-8372-148833c4be22&host=cosmic-gray
A recheck that came during daemonReady reused the settled lookup and its
old sessions read, so a stale daemon that went quiet stayed until an
unrelated lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d26-06d5-7823-8372-148833c4be22&host=cosmic-gray
The detached --version task returns nil when the process does not start;
the closure needs its type for that (fast tier: 'nil' requires a contextual type).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d26-06d5-7823-8372-148833c4be22&host=cosmic-gray
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (independent subagent): SHIP at ac9d19e

Second rebase of 020c8bd (reviewed SHIP) onto feat-cmux-next. Commits 2 to 5 are identical in the range-diff. Commit 1 differs only in TunableCatalog.all, which is base's list (now with BrowserPageTunables.all) plus DaemonTunables.all once. In all 34 files the PR's added and removed lines hash the same as before. No base code added since calls an API this PR changed or adds a type this PR adds.

teamleaderleo and others added 3 commits October 21, 2026 12:02
The quiet restart of a stale local cmux-tui never fired: the daemon's own
agent screen detector (a cmux SDK client) holds two connections with no
name and no kind for its whole life, and `.reconnecting` took every
kindless local client for a `cmux` CLI call in flight. In the app the
probe was never idle, so the restart waited until the overdue toast;
QuietUpdateKeepsTerminalTests failed on it in CI and 3 of 3 times on
cmux16 (client.list showed the two kindless clients, 30 s old).

A kindless local client now holds the restart while it is younger than
`callSeconds` (a minute; the quiet restart tries again each minute). The
live test waits until the daemon's standing clients are older than its
10 s window, then proves a fresh CLI call still holds the restart and
the terminal survives it. It reports what the probe saw when it fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d26-06d5-7823-8372-148833c4be22&host=cosmic-gray
Under load the first try's task can start after the test sets the owner
idle; it then restarts before the client-detached event the test means
to drive it (seen once on cmux16: restarts [11] at line 110). The test
counts owner probes and waits for the first one before it changes the
owner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?harness=claude&session=01a15d26-06d5-7823-8372-148833c4be22&host=cosmic-gray
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (independent subagent): SHIP at da84a7b

On top of ac9d19e (reviewed SHIP). 4f7d43f fixes a real bug that CI's first test run found. OwnerIdleProbe counted the daemon's own agent-screen-detector connections (kindless and permanent) as CLI calls in flight, so the quiet restart could never fire. Now a kindless local client holds it only while connected_seconds < 60, which the reviewer checked against client.list's wire format (a decimal string). The trade-off: a kindless call older than a minute, such as cmux agents wait, gets a retryable error at the handoff; terminals survive. c36a5ec and da84a7b make the busy-owner test wait for, and require, its first probe, and add the doc cross-references. On cmux16, QuietUpdateKeepsTerminalTests passed 3/3 and QuietDaemonUpdateTests 5/5.

@blacksmith-sh

blacksmith-sh Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Found 11 test failures on Blacksmith runners:

Failures

Test View Logs
/tmp/tmp6_1iep77/summary-fail.json (live):/summary-fail.json (live): View Logs
/tmp/tmpvxbpuqdi/summary-missing.json (live):/summary-missing.json (live): View Logs
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows/
[new_workspaces] debug.new_workspace timed 0 of 50 new workspace rows
View Logs
[new_workspaces] the window draws no frames/[new_workspaces] the window draws no frames View Logs
[tabs] debug.tab_switch timed 0 of 40 switches/
[tabs] debug.tab_switch timed 0 of 40 switches
View Logs
[tabs] the window draws no frames/[tabs] the window draws no frames View Logs
convention violations found (ERROR lines above)./
convention violations found (ERROR lines above).
View Logs
test_base_passes (main.NoticesPairTests.test_base_passes)/
test_base_passes (main.NoticesPairTests.test_base_passes)
View Logs
test_every_submodule_free_checkout_on_an_owned_runner_resets_submodules (main.Reuse
dWorkspaceSubmodules.test_every_submodule_free_checkout_on_an_owned_runner_resets_submo
dules)/
test_every_submodule_free_checkout_on_an_owned_runner_resets_submodules (main.Reuse
dWorkspaceSubmodules.test_every_submodule_free_checkout_on_an_owned_runner_resets_submo
dules)
View Logs
test_every_submodule_free_checkout_on_an_owned_runner_resets_submodules (main.Reuse
dWorkspaceSubmodules.test_every_submodule_free_checkout_on_an_owned_runner_resets_submo
dules) (job='web-bundles')/
test_every_submodule_free_checkout_on_an_owned_runner_resets_submodules (main.Reuse
dWorkspaceSubmodules.test_every_submodule_free_checkout_on_an_owned_runner_resets_submo
dules) (job='web-bundles')
View Logs

...and 1 more test failures. View all on Blacksmith

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next crash-class rules / cmux-next crash-class rules is red on 10 of 10 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next guards / nightly-guards / workflow-guard-tests / release-notary is red on 19 of 19 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next / cmux-next checks (god files, concurrency, crash safety, l10n) › Checks job structure is red on 14 of 14 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next / cmux-next checks (god files, concurrency, crash safety, l10n) › No fleet host names in gallery fixtures is red on 17 of 17 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

cmux-next / cmux-next checks (god files, concurrency, crash safety, l10n) › No new SwiftUI (macOS UI is AppKit) is red on 15 of 17 fresh feat-cmux-next PR heads in the last 2:00:00, so it is most likely a base red, not this PR's. The base-reds lane has it; a base merge picks up the fix once it lands.

teamleaderleo added a commit that referenced this pull request Oct 26, 2026
…t wired yet)

AcpmuxUnresponsiveWatch counts status calls that miss their deadline and
stops the daemon only it found (pid, start time, audit token). This commit
does not call it from the lookup yet, so AcpmuxUnresponsiveRestartTests
fail: a daemon that is alive but never answers keeps its lock and no new
daemon starts.

Rebuilt from #23199 (daemon-autoupdate 05367c0) on current feat-cmux-next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
…not wired yet)

UnresponsiveOwnerWatch reads every `server ensure` outcome and, after two
stuck ensures over 30 s, replaces the owner with `--replace-unresponsive`
(once per owner process, three times per launch, never a `server.busy`
owner). This commit does not attach it to the launcher yet, so
DaemonAutoRestartTests.aStuckOwnerIsReplacedWithNoConfirmAfterTheWindow
fails: only the Restart button replaces a stuck owner.

Rebuilt from #23199 (daemon-autoupdate 05367c0) on current feat-cmux-next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
A Cloud machine becomes a DaemonUpdateTarget: the same `cmux link machine
update` as Update Machine, with `--to-bundled`, so a machine on any other
build than the app's gets the app's own pinned build. It waits for a quiet
moment (no person typing, no chat or terminal UI attached, no `cmux` call
in flight, no terminal agent or chat in a turn) and the queue updates it
mid-turn once it stayed busy 10 minutes; a machine whose daemon the app
refused updates at once. cloud.updateMachines (Debug Settings, next to
agent.updatePeers) turns it off. Nothing offers these targets yet: the
app's handshake follower and the header line come next.

The quiet read's client rule now leaves out a kindless local client
connected for over a minute (the daemon's own agent screen detector), as
the review of #23199 found for the local probe; #24070's rule counted it
and a machine with one was never quiet.

From #24070 (daemon-peer-update bd4892c).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
…t wired yet)

AcpmuxUnresponsiveWatch counts status calls that miss their deadline and
stops the daemon only it found (pid, start time, audit token). This commit
does not call it from the lookup yet, so AcpmuxUnresponsiveRestartTests
fail: a daemon that is alive but never answers keeps its lock and no new
daemon starts.

Rebuilt from #23199 (daemon-autoupdate 05367c0) on current feat-cmux-next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
A Cloud machine becomes a DaemonUpdateTarget: the same `cmux link machine
update` as Update Machine, with `--to-bundled`, so a machine on any other
build than the app's gets the app's own pinned build. It waits for a quiet
moment (no person typing, no chat or terminal UI attached, no `cmux` call
in flight, no terminal agent or chat in a turn) and the queue updates it
mid-turn once it stayed busy 10 minutes; a machine whose daemon the app
refused updates at once. cloud.updateMachines (Debug Settings, next to
agent.updatePeers) turns it off. Nothing offers these targets yet: the
app's handshake follower and the header line come next.

The quiet read's client rule now leaves out a kindless local client
connected for over a minute (the daemon's own agent screen detector), as
the review of #23199 found for the local probe; #24070's rule counted it
and a machine with one was never quiet.

From #24070 (daemon-peer-update bd4892c).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
…t wired yet)

AcpmuxUnresponsiveWatch counts status calls that miss their deadline and
stops the daemon only it found (pid, start time, audit token). This commit
does not call it from the lookup yet, so AcpmuxUnresponsiveRestartTests
fail: a daemon that is alive but never answers keeps its lock and no new
daemon starts.

Rebuilt from #23199 (daemon-autoupdate 05367c0) on current feat-cmux-next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
…swering (#25379)

* cmux NEXT acpmux watchdog: tests for a hung daemon, and the watch (not wired yet)

AcpmuxUnresponsiveWatch counts status calls that miss their deadline and
stops the daemon only it found (pid, start time, audit token). This commit
does not call it from the lookup yet, so AcpmuxUnresponsiveRestartTests
fail: a daemon that is alive but never answers keeps its lock and no new
daemon starts.

Rebuilt from #23199 (daemon-autoupdate 05367c0) on current feat-cmux-next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux NEXT acpmux watchdog: a stuck handshake signals the hung daemon and starts a new one

When the daemon accepts the connection but never answers within the deadline, the
host asks the unresponsive watch to restart it: SIGTERM to the hung pid (SIGKILL
after its grace), then a fresh daemon, and the interrupted turn resumes on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux NEXT acpmux watchdog: tests for an answer during the confirm, the toggle off before the claim, a lookup with it off

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux NEXT acpmux watchdog: claim only a streak that still stands, read the toggle at each miss and before the claim

A claim needs the streak that made the restart due: an answer meanwhile (another lookup) ends
it, so a daemon that recovered is never signalled. The watch follows this app's daemon also
with the toggle off, so answers always end a streak; the toggle is read at each miss (off ends
the streak) and again right before the claim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux NEXT acpmux watchdog: a claim names the streak that made the restart due

An answer during one lookup's confirm, then fresh misses, begin a new streak; the old
confirm's claim names its own streak, so it neither takes the new one before its window nor
removes it. aDaemonThatAnsweredMeanwhileIsNotClaimed covers answer, new misses, old claim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Oct 26, 2026
…ut never answers (#25382)

* cmux NEXT cmux-tui watchdog: tests for a stuck owner, and the watch (not wired yet)

UnresponsiveOwnerWatch reads every `server ensure` outcome and, after two
stuck ensures over 30 s, replaces the owner with `--replace-unresponsive`
(once per owner process, three times per launch, never a `server.busy`
owner). This commit does not attach it to the launcher yet, so
DaemonAutoRestartTests.aStuckOwnerIsReplacedWithNoConfirmAfterTheWindow
fails: only the Restart button replaces a stuck owner.

Rebuilt from #23199 (daemon-autoupdate 05367c0) on current feat-cmux-next.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux NEXT cmux-tui watchdog: replace a stuck owner with no confirm

DaemonRecovery follows the launcher's ensure outcomes with
UnresponsiveOwnerWatch: once the owner stayed alive but unanswering for the
sustained window (30 s, two stuck ensures), the app runs the button's
restart without its confirm, at most three times per launch, while
cmuxTUI.restartUnresponsive is on. aStuckOwnerIsReplacedWithNoConfirmAfterTheWindow
passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux NEXT cmux-tui watchdog: stop with the service, claim before replacing, no window while off

Review of ddf4bb5 (Codex):
- The watch outlived DaemonService.shutdownConnection and could replace an
  owner after it; shutdownConnection now detaches it (UnresponsiveOwnerWatch.stop:
  the deadline is cancelled and nothing is claimed after).
- A replace queued while the owner was stuck ran even after the toggle went
  off, an ensure worked or the person's Restart ran; the replacer now
  claims it on the main actor right before restarting, and a claim that
  says no gives the replace back.
- The deadline was scheduled and cancelled after the state lock, so an
  older outcome could cancel a newer streak's deadline; both now happen
  under the lock.
- With the toggle off stuck ensures still built a streak and a deadline;
  now a stuck ensure with the toggle off ends the streak and cancels it, and
  a deadline that fires with the toggle off ends the streak.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux NEXT cmux-tui watchdog: tests for a refused replace, the toggle off between ensures, a claim that finds it off

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux NEXT cmux-tui watchdog: give back a replace that never claimed, follow the toggle as it changes

A replace the recovery refused before its claim (the person's Restart was running) is given
back like a claim that says no, so the same owner, still stuck, is replaced later. The toggle's
changes reach the watch through an ObservationStream (cancelled by the next attach and by
detach): off ends the streak and its deadline at once, and a claim that finds it off ends the
streak too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux NEXT cmux-tui watchdog: the toggle is read, not followed

ObservationStream coalesces a toggle turned off and on again into no change, so following it
could not promise more than reading it. The window measures the owner: the toggle is read at
each stuck ensure, when the window passes and at the claim, off at any of them ends the streak,
and the replace runs only when the claim on the main actor reads it on. The follow task is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux NEXT cmux-tui watchdog: the claim closure takes its own weak reference

The nested closure referred to the Task's captured weak self, which Swift 6 refuses in
concurrently-executing code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Closing: rebuilt on current feat-cmux-next as smaller PRs, now merged:

Base already moves a stale local acpmux and cmux-tui to the bundled build (AcpmuxVersionHandoff.handOffIfStale, #25219; DaemonLauncher.handOffIfStale). Following Leo's order ("OUR SHIT IS HARDLY EVER GOING TO GO IDLE ... IT RESUMES WHATEVER IT INTERRUPTED"), the quiet-wait handoff (AcpmuxQuietHandoff) was not rebuilt.

teamleaderleo added a commit that referenced this pull request Oct 26, 2026
…er from restarting (#25513)

* OwnerRestartWaitsForChatTests: a CLI call without a kind holds the Chief owner restart (red)

The DEV keyless Chief owner restart (e859e33) probes the owner with
OwnerIdleProbe, which counts a fresh local client that names no kind as
idle. A `cmux` CLI or MCP call names no kind, so the owner restarts in the
middle of its request. This test opens such a call and expects the restart
to wait while it is connected and to run after it ends. It fails on this
commit: attempt() returns .started.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a

* OwnerIdleProbe: a fresh CLI or MCP call without a kind keeps the Chief owner busy

The DEV keyless Chief owner restart (e859e33, c6390e5, 79ce2f2) asked
OwnerIdleProbe whether the owner is idle, and the probe let every local
client without a kind through. A `cmux` CLI or MCP call names no kind, so
IdleOwnerRestart could restart the owner in the middle of its request.

As in 4f7d43f (the quiet restart's fix on the closed #23199 branch), a
local client without a kind now holds the restart while it is younger than
callSeconds (a minute). An older one is a standing connection: the daemon's
own agent screen detector holds two for its whole life and must not hold
the restart forever. Such a client stops counting as a call without any
owner event, so when the probe sees only calls in flight it answers
callInFlight(seconds:) and IdleOwnerRestart schedules one more attempt for
then (a one-shot DemandTimer, cancelled when the outcome is decided and
with the actor). A call that ends sooner sends client-detached, which
probes again as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a

* OwnerRestartWaitsForChatTests: the CLI-call test waits for the new owner before it shuts down

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent-Session: https://cmux.com/deeplink/agent?id=aid-1288bbb2b5d2f5c6b66a

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

This branch was previously deployed

1 inactive deployment
ci-routing-agent — d022ab1d Deployed Oct 22, 2026 by teamleaderleo via namespace autofix #1532
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk:high Risk tier (docs/ci/risk-tiers.md)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant