Skip to content

cmux NEXT Cloud: a first chat an older daemon refused updates the machine and starts once more - #29497

Merged
teamleaderleo merged 6 commits into
feat-cmux-nextfrom
lane/cloud-first-start-updates-machine
Oct 31, 2026
Merged

teamleaderleo merged 6 commits into
feat-cmux-nextfrom
lane/cloud-first-start-updates-machine

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 31, 2026 •

Copy link
Copy Markdown
Collaborator

Leo: "whenever i start a chat it should be able to send me to cloud automatically".

What was wrong

A freshly created Cloud machine refused its creator's Claude chat with agent_session.team_vm_blocked ("Agent chats cannot be started from this Mac on a team VM yet"). The refusal came from the machine's own cmux-tui.

What changes

A start that the machine's older daemon refuses with team_vm_blocked or host_unverified now updates the machine and starts once more. This only happens on the caller's own machine, meaning the link's owner answer is present.

  • DaemonUpdatePolicy.updateNow(_:) is new.
    • It queues the machine at the front of the shared policy's queue and returns once that turn ends.
    • Like the person's Update click, neither the ledger nor this run's memory holds it back.
    • It joins an update of that machine that is already running instead of running a second one.
    • It returns at once while agent.updatePeers is off, or when the start is cancelled.
  • AgentTabRemoteHost.updateMachine is new.
    • keyedStart runs it on those two refusals, re-runs the start's proceed check (the account check), then tries the start once more.
    • Every other refusal, and a refusal with no update ran, stands as before.
  • CloudMachineUpdates.updateNow builds the same target as the background offer, so the update keeps the same guarantees:
    • --if-older, never a move back;
    • the app's own pinned build;
    • terminals and chats keep running, and a rollback happens if the new daemon does not serve.
  • The wiring sets updateMachine only for Cloud machines. It calls it only while the link's owner answer is present. A teammate's machine is still refused by the app before any start is sent, and is never retried.

The new logic stays in the existing Swift update policy and host. The update itself is already Rust (cmux link machine update), and this adds only the "wait for it, then start again" glue around it.

Tests

  • DaemonUpdatePolicyTests covers updateNow:
    • it updates the machine and returns after the turn;
    • an earlier failure on this build does not hold it back;
    • it joins an update that is already running (one call, not two);
    • it returns at once with the toggle off.
  • CloudStartSessionTests.aStartAnOlderDaemonRefusedUpdatesTheMachineAndStartsOnceMore covers:
    • each of the two codes leads to update, proceed, then a second start;
    • no update, or a refusal outside the set, leads to one start only.

Risk

  • A refused first start on an outdated Cloud machine now waits for that machine's update, which takes up to a few minutes while the chat shows starting. Before, it failed at once.
  • The update runs mid-turn, as the automatic update already does (rulebook: "THE DAEMON UPDATE ... RESUMES WHATEVER IT INTERRUPTED").
  • No timer or observer is added. Each wait ends when its target's turn ends, when the queue drops the target, or when the task is cancelled.

Not in this PR

  • Capture. The pr-capture Cloud fixture pinned the same 308bd36 on its own machine (cmuxterm-hq#5399), and a DEV capture app cannot update machines because it ships no signed pin. The nightly signed-in leg (hq run 41207279977) therefore failed by construction. A cmuxterm-hq PR makes the fixture's machine run the app's own bundled cmux-tui commit.
  • Image. No workflow rebakes the Cloud image when cmux-tui moves. The pin, bake, promote and channel PR are all manual. Filed as a follow-up.

Agent session: agent session

🤖 Generated with Claude Code


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


Summary by cubic

A first chat on a fresh Cloud machine now survives the machine's older daemon: when the daemon refuses the start with team_vm_blocked or host_unverified, the app updates the machine to its own build and starts once more.

  • A new machine boots its image's cmux-tui, which predates the owner-chat support the app's start relies on; the automatic background update started at handshake usually lost a race to the first chat.
  • The retry queues the machine at the front of the shared daemon update policy, due now and joining an update of that machine that already runs; it waits for that turn to end on a capped stream, including the next turn when a joined one ends before its update ran, then re-runs the account check before starting again.
  • A refusal that joins a running update never runs a second one, even when that update fails; the joined turn itself answers the refusal.
  • The retry happens only on the caller's own machine, where the link's owner answer is present; teammate machines, and any other refusal, behave as before.
  • A refused first chat now waits up to a few minutes for the machine's update instead of failing immediately.

Written for commit 7cc32b6. Summary will update on new commits.

View guided diff Turn on auto-fix

…hine and starts once more

A fresh Cloud machine boots its image's cmux-tui (images/cmux-vm pins 308bd36, from
before #29107), so its daemon refuses its creator's chat on a team VM with
agent_session.team_vm_blocked. The app's automatic update of that machine runs in the
background from its handshake, so a chat started right after the machine came up
(cloud.startChat, cmux cloud start-session, the first send) reached the old daemon.

Now such a refusal (team_vm_blocked or host_unverified) on the caller's own machine (the
link's owner answer) runs the same update at the front of the shared DaemonUpdatePolicy
queue (DaemonUpdatePolicy.updateNow, joining an update of that machine that already
runs), waits for its turn to end, and starts once more. A teammate's machine is never
retried; any other refusal stands.

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

cursor Bot commented Oct 31, 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.

@github-actions

github-actions Bot commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

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

Review 7cc32b6: 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=pr29497 -f ref=lane/cloud-first-start-updates-machine -f platform=macos -f backend_mode=local

web and size checks: green

Shipped in:

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Owned by an agent session · session

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

Copy link
Copy Markdown
Collaborator Author

Review (codex, router-review): SHIP at a48ab78

Reviewer: codex gpt-6-astra high, fast tier.

Follow-ups: 2. Each lands as an issue in this repo that links this receipt; the lane that opened the PR fixes it forward.

SHIP at a48ab78

Visual verdict: no images; Leo tries the build

  1. [P2] [follow-up] DaemonUpdatePolicy.swift:194: Repeated “update now” requests retain backoff. After an urgent attempt returns .busy, its queued entry remains urgent with a future deadline. Another updateNow calls enqueue, which resets that deadline only when promoting a nonurgent entry. A subsequent chat can therefore wait up to 30 minutes before its update begins. Explicit immediate requests should reset the queued deadline and wake the sleeper.

  2. [P2] [follow-up] DaemonUpdatePolicy.swift:189: Joining a running turn can bypass the promised forced retry. running covers bookkeeping as well as the actual update. If a background turn is awaiting currentBuild() for a previously unsupported machine, updateNow joins without setting retried; that turn then skips the unchanged build using cannotUpdate. The chat retries against the same old daemon without an update. Distinguish an active update from a turn still checking eligibility, and ensure the latter honors the explicit retry.

Static review completed; Swift tests could not run because this Linux box has no Swift toolchain.

Follow-ups:

  • Reset backoff for every explicit updateNow request.
  • Ensure joining a bookkeeping turn still forces the requested update.

…st is due now and outlasts a turn that never reached its update

Review of #29497 (follow-ups #29498, #29499) and the concurrency gate's unbounded AsyncStream.

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

cursor Bot commented Oct 31, 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

Review (codex, router-review): SHIP at c85e0f1

Reviewer: codex gpt-6.1-sol xhigh, fast tier.

Follow-ups: 2. Each lands as an issue in this repo that links this receipt; the lane that opened the PR fixes it forward.

SHIP at c85e0f1
Visual verdict: no images; Leo tries the build

  1. [P1] [follow-up] Joined updates can run twice. If updateNow arrives while target.update is already in flight, retried remains set and the completed turn requeues the machine at DaemonUpdatePolicy.swift:318 and DaemonUpdatePolicy.swift:320. The Cloud reconnect is asynchronous, so the next turn can still observe the old build and invoke another update/restart, defeating the “one call, not two” guarantee.

  2. [P1] [follow-up] Cancellation can still start the chat. updateNow finishes its wait on cancellation, but CloudMachineUpdates.updateNow returns true unconditionally at CloudMachineUpdates.swift:98. keyedStart then runs the retry at AgentTabRemoteHost.swift:154 and AgentTabRemoteHost.swift:155 without checking cancellation. The first-send path uses proceedAlways, so closing/canceling while the update is running can still issue agent-session-start and create a remote chat.

Follow-ups:

  • [P1] [follow-up] Prevent a joined, already-running update from being requeued as a second update.
  • [P1] [follow-up] Stop the retry when the start task is canceled.

…first-start-updates-machine

# Conflicts:
#	Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabRemoteHost.swift
#	Packages/macOS/CmuxNext/Sources/CmuxNextApp/AgentTabs+Wiring.swift
@blacksmith-sh

blacksmith-sh Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Found 4 test failures on Blacksmith runners:

Failures

Test View Logs
main.DeveloperIdAppTests.test_download_prints_the_active_profile_only/
test_download_prints_the_active_profile_only
View Logs
stubs: /System/Library/Frameworks/Security.framework/Versions/A/Security has no stub/
Security has no stub
View Logs
stubs: CoreFoundation.tbd: install-name /System/Library/Frameworks/
CoreFoundation.framework/Versions/A/CoreFoundation, missing 1: ['_CFRetain']/
CoreFoundation, missing 1: ['_CFRetain']
View Logs
stubs: CoreFoundation.tbd: install-name /System/Library/Frameworks/
CoreFoundation.framework/Versions/A/CoreFoundation, missing 1: ['_log2']/
CoreFoundation, missing 1: ['_log2']
View Logs

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

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex, router-review): SHIP at e154c9d

Reviewer: codex gpt-6.1-sol xhigh, fast tier.

Follow-ups: 2. Each lands as an issue in this repo that links this receipt; the lane that opened the PR fixes it forward.

SHIP at e154c9d

Visual verdict: no images; Leo tries the build

  1. [P1] [follow-up] CloudMachineUpdates.swift:97 waits for the policy but then always returns true. The policy can return without running a turn (cancellation, toggle changes, missing/dirty bundled build), so keyedStart retries at AgentTabRemoteHost.swift:156 even though no update happened. Propagate whether a turn actually ran and honor cancellation before the retry.

  2. [P1] [follow-up] CloudMachineUpdates.swift:97 considers the update complete before the Cloud daemon reconnects. reconnectAfterUpdate only schedules connect() (CloudMachineSession.swift:377); the immediate retry can still use the old app-link endpoint/ticket while the machine remains updateRequired, causing the start to fail instead of waiting for the new daemon handshake.

Follow-ups:

  • [P1] [follow-up] Return an accurate update/no-op/cancel status and prevent retry after cancellation.
  • [P1] [follow-up] Await the Cloud daemon’s post-update reconnect before retrying the chat start.

@teamleaderleo teamleaderleo removed the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Review (codex gpt-6-astra high, router-review): BLOCK at 872a507

BLOCK at 872a507

Visual verdict: no images; Leo tries the build

  1. [P1][blocking] CloudMachineUpdates.swift:97 calls DaemonUpdatePolicy.shared, but DaemonUpdatePolicy has no shared member. The app’s policy is services.daemonUpdates.policy, so this change does not compile.

  2. [P1][follow-up] DaemonUpdatePolicy.swift:185-199 can schedule a second update when updateNow joins a turn already inside target.update. attempt removes retried immediately before awaiting the update (DaemonUpdatePolicy.swift:411-412); a refusal arriving during that await inserts it again, and the turn-end logic requeues the target (DaemonUpdatePolicy.swift:312-313). A failed or rolled-back update can therefore run twice, contrary to the join guarantee.

  3. [P2][follow-up] No after screenshots were supplied, so visual verification was unavailable.

Follow-ups:

    1. Fix the running-turn race so joining never schedules an extra update.
    1. No visual artifacts were available.

@teamleaderleo teamleaderleo removed the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
…g update never runs it twice

Review of #29497 at 872a507: #28271 removed DaemonUpdatePolicy.shared (the
policy is services.daemonUpdates.policy), and a refusal that arrived while the
machine's update was inside target.update re-marked it, so a failed update ran
a second time. The policy now marks the target inside its update and updateNow
only waits for that turn.

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

Copy link
Copy Markdown
Collaborator Author

Review (codex, router-review): SHIP at 7cc32b6

Reviewer: codex gpt-6-astra high, fast tier.

Follow-ups: 1. Each lands as an issue in this repo that links this receipt; the lane that opened the PR fixes it forward.

SHIP at 7cc32b6

Visual verdict: no images; Leo tries the build

  1. [P2] [follow-up] DaemonUpdatePolicy.swift:208 — The second-turn wait races with the drain. When updateNow joins a turn already reading cannotUpdate, that turn can skip updating and enqueue another turn. The drain can then enter attempt, which removes retried at line 358, before this waiter resumes. Consequently, this check sees no retry marker and returns while the required update is still pending. The chat spends its only retry against the old daemon and fails again. Track the requested turn’s completion independently of retried, and test joining during the cannotUpdate read with the subsequent update held open.

Source review completed; tests could not run because this Linux box has no Swift toolchain.

Follow-ups:

  • Fix the second-turn completion race and add a deterministic test.

@teamleaderleo teamleaderleo removed the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@github-actions github-actions Bot added the risk:normal Risk tier (docs/ci/risk-tiers.md) label Oct 31, 2026
@teamleaderleo
teamleaderleo merged commit 270912b into feat-cmux-next Oct 31, 2026
213 of 217 checks passed
@teamleaderleo
teamleaderleo deleted the lane/cloud-first-start-updates-machine branch October 31, 2026 18:38
lawrencecchen pushed a commit that referenced this pull request Oct 31, 2026
… older-daemon codes, moves to CmuxNextDaemonService (app_lines +51) (#29590)

merge-batch candidate: the squash merge of #29590 at bc518d0
cmux-bot Bot pushed a commit that referenced this pull request Oct 31, 2026
… older-daemon codes, moves to CmuxNextDaemonService (app_lines +51) (#29590)

* base red: Takes back #29497: the remote-start refusal table, with its older-daemon codes, moves to CmuxNextDaemonService (app_lines)

270912b (#29497, "a first chat an older daemon refused updates the
machine and starts once more") adds 51 CmuxNextApp Swift lines with no
exception row; arch-ratchet's app_lines row refuses it alone (--base
270912b^ --head 270912b: +51), part of the tip's red (STATUS, lead
18:5xZ).

- #29497's own refusal codes (AgentTabRemoteHost.olderDaemonRefusals) move,
  with their doc, onto the refusal type as
  AgentRemoteStartRefusal.olderDaemonCodes.
- The refusal type itself (AgentRemoteStartRefusal: the one table from an
  `agent-session-start` refusal to the person's words, through RefusalStrings)
  and AgentTabRefusedHost move unchanged in behavior from
  CmuxNextApp/AgentTabs+RemoteStart.swift to CmuxNextDaemonService, next to
  RefusalStrings, whose Refusals table they already read (same bundle, same
  keys). Public access, an explicit init with the same defaults.
The App reaches them through its CmuxNextDaemonService re-export; the tests
are unchanged.

* The folder-needed code keeps its App declaration (defaults guard); the moved refusal uses its own constant, a test keeps them equal

The defaults guard tracks AgentRemoteStartRefusal.folderNeededCode by its
file path (it is referenced from CloudStartSession.startFailure) and reported
its move as a changed default. The App keeps
`static let folderNeededCode = "app.folder_needed"` in
AgentTabs+RemoteStart.swift, as an extension of the moved type; the moved
type uses an internal appFolderNeeded for folderNeeded(machine:) and its
message. AgentRemoteStartRefusalCodeTests asserts the two are the same code.

This branch was previously deployed

1 inactive deployment
ci-routing-agent — 7cc32b64 Deployed Oct 31, 2026 by teamleaderleo via cmux-tui tree preflight #90964
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant