Skip to content

fix(ios): let a legacy untagged Mac row take pushed presence routes - #14012

Merged
teamleaderleo merged 1 commit into
mainfrom
fix/ios-legacy-row-presence-routes
Sep 23, 2026
Merged

teamleaderleo merged 1 commit into
mainfrom
fix/ios-legacy-row-presence-routes

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Sep 23, 2026 •

Copy link
Copy Markdown
Collaborator

A Mac paired before instance tags existed has an untagged stored row, and presence always reports a tag. Since 5aedb51, performPushedRouteSyncBatch looks up the stored row by the exact pairing id (device + presence tag), so it never finds that row. As a result, a legacy Mac that changes address never gets the new routes pushed over presence. This PR falls back to the device's untagged row when no exact tagged row exists.

The receiving side was already written for this case. For an untagged row, applyPushedRoutes calls reconnectRouteAuthority(pairedMacInstanceTag: nil), which accepts only the device's sole route-advertising build. The upsert is still conditioned on .matchingInstanceTag(nil). So when two builds on one Mac both advertise routes, nothing is written; presenceRoutesDoNotFanOutWhenLogicalDuplicatesBothAdvertiseRoutes covers that and still passes.

This is the mobile-core-package hang

MobileShellCompositePairedMacCoalescingTests.presenceRoutesForHiddenDuplicateRefreshOnlyTheEmittingRow() covers this path. On main it awaits pairedStore.waitUntilUpsertCount(1), which has no bound, and the upsert never comes. The run below reproduced it.

  • On air-blue (Xcode 27.0) at upstream main db5d212d86, running this one test alone hangs until killed (timeout 150 exit 124). A serial full-package run parked on it at 0% CPU; a sample showed every thread idle in mach_msg/workq.
  • In fork run 35876818909, the job log goes silent right after MobileShellAltScreenNoticeTests.sameActiveScreenRenderGridDoesNotNotifyAlternateScreenObservers() started, and the job then hits the 25-minute timeout. That line is a flush boundary, not the hung test. The job's output arrived in about 64 KB blocks, and the alt-screen test passed in both serial runs on air-blue. Suite order is the same on both machines, and this test runs a few suites later. I could not rerun the job with unbuffered output, so the claim that CI hung on this same test rests on this reasoning, not on a CI run.

Testing

  • With this change, on air-blue: all nine MobileShellCompositePairedMacCoalescingTests pass, including the formerly hanging test (0.03 s).
  • A full serial run of the package is in progress. I will add its result as a comment.
  • Not run under Xcode 26.6.

waitUntilUpsertCount should probably have a deadline so a regression here fails the test instead of the job. I left that out to keep this PR to the product fix.

— Thimble g1 🔆
Run: run_ios_shell_mobile_tests_20260923_e464e5ba

🤖 Generated with Claude Code


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


Summary by cubic

Fixes pushed presence route sync for legacy Macs paired before instance tags existed.

  • The lookup now falls back to the device's untagged row when no exact tagged pairing exists, so a moved legacy Mac gets fresh routes pushed.
  • applyPushedRoutes still restricts untagged rows to the device's sole route-advertising build, so a Mac with two builds online is not written.
  • The previously hanging test presenceRoutesForHiddenDuplicateRefreshOnlyTheEmittingRow now passes; the hang was an unbounded wait for an upsert that never arrived.

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

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved pushed route syncing for devices paired using the legacy, untagged pairing format.

performPushedRouteSyncBatch has looked up the stored row by the exact
pairing id (device + presence tag) since 5aedb51. A row paired before
instance tags existed is untagged, and presence always reports a tag, so
the lookup never found it. applyPushedRoutes was handed nil and never wrote.
Its untagged branch, reconnectRouteAuthority(pairedMacInstanceTag: nil),
which lets a legacy row adopt its device's sole route-advertising build,
had become unreachable. A legacy Mac that moved stopped getting fresh
routes over presence.

Fall back to the device's untagged row when there is no exact tagged
match. applyPushedRoutes still requires the presence instance to be the
device's only route-advertising build, and the upsert is still conditioned
on the row's own (nil) tag, so a Mac with two builds online is not written.

presenceRoutesForHiddenDuplicateRefreshOnlyTheEmittingRow covers this. On
main it awaits pairedStore.waitUntilUpsertCount(1) forever, which is what
hangs the serial CmuxMobileShell package run until the job timeout.

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

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 54439842-c7be-42a4-b022-4661e50fa27d

📥 Commits

Reviewing files that changed from the base of the PR and between 3ca19ad and 7dc36c2.

📒 Files selected for processing (1)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PresenceRouteSync.swift

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


📝 Walkthrough

Walkthrough

The pushed-route sync batch now falls back to an untagged legacy pairing when it cannot find a paired Mac for the primary pairing ID. Route application receives the primary paired Mac when available.

Changes

Legacy pairing route sync

Layer / File(s) Summary
Pairing fallback for route sync
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PresenceRouteSync.swift
The batch computes an untagged legacy pairing ID. If the primary paired Mac is absent, it passes the legacy paired Mac to route application. The legacy row writes only when the instance is that device’s sole route-advertising build.

Estimated code review effort: 2 (Simple) | ~8 minutes

Suggested reviewers: azooz2003-bit

Merge Risk: ⚪ Minimal · up to 7dc36

The legacy pairing fallback retains the existing route-authority check. No actionable issue remains before normal merge checks.

🚥 Pre-merge checks | ✅ 23 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description clearly explains the problem, implementation, scope, and test results. However, it omits the template's Demo Video, Review Trigger, and Checklist sections, including the required deter… Add the missing template sections. Include a demo video or explain why one is not applicable, provide the review trigger block, complete the checklist, and document deterministic soak coverage or explain why existing coverage applies.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 1 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (23 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: allowing legacy untagged Mac rows to receive pushed presence routes.
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.
Cmux Cloud Persistent Session And Early Input ✅ Passed PASS: The only pull-request change adds a fallback from a tagged pairing ID to the same device's untagged legacy pairing row before applyPushedRoutes. It does not create Cloud terminals, cmux-tui …
Cmux Swift Actor Isolation ✅ Passed PASS: The only production change adds a local legacy pairing-ID value and a fallback dictionary lookup inside the existing @MainActor extension MobileShellComposite. It introduces no model, protocol…
Cmux Swift Blocking Runtime ✅ Passed PASS. The PR changes one production Swift file and only adds a fallback MobilePairedMac.pairingID(..., instanceTag: nil) lookup. The diff adds no semaphore, blocking wait, sleep, delayed dispatch, p…
Cmux Browser Automation Off-Main ✅ Passed PASS. The pull request changes only MobileShellComposite+PresenceRouteSync.swift and adds a legacy pairing-ID fallback for pushed presence routes. The diff contains no browser socket automation, Web…
Cmux Expensive Synchronous Load ✅ Passed The diff only adds an untagged MobilePairedMac pairing-ID fallback in performPushedRouteSyncBatch. It does not add or move any agent-history loader, transcript/trajectory/workstream file read, bro…
Cmux Cache Substitution Correctness ✅ Passed The diff does not replace a fresh authoritative read with a cache. It adds an untagged pairing-ID fallback to the existing lookup. The path still calls loadPairedMacs(forceRefresh: true) before buil…
Cmux No Hacky Sleeps ✅ Passed PASS. The custom rule covers TypeScript, JavaScript, shell, and non-Swift build/runtime scripts. The PR changes only MobileShellComposite+PresenceRouteSync.swift. The patch adds a legacy pairing-ID …
Cmux Algorithmic Complexity ✅ Passed The authoritative diff changes only MobileShellComposite+PresenceRouteSync.swift. Inside the existing hostInstances batch loop, it adds a second MobilePairedMac.pairingID calculation and a fallb…
Cmux Swift Concurrency ✅ Passed The pull request changes only the paired-Mac lookup inside the existing async route-sync operation. The diff adds a legacy pairing-ID calculation and a nil-coalescing fallback; it adds no Dispatch que…
Cmux Swift @Concurrent ✅ Passed The diff only adds two synchronous MobilePairedMac.pairingID lookups and a dictionary fallback inside the existing @MainActor performPushedRouteSyncBatch path. It does not add or change `@concur…
Cmux Swift Package Boundaries ✅ Passed PASS: The PR changes only a private lookup inside MobileShellComposite+PresenceRouteSync.swift. The changed code selects an exact MobilePairedMac row or an untagged legacy row before calling the e…
Cmux Swiftpm Lockfiles ✅ Passed PASS: The reviewed range changes only Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PresenceRouteSync.swift. It does not change Package.swift, package dependency pins, …
Cmux Swift Logging ✅ Passed PASS: The PR changes only pairing-ID lookup logic and comments in performPushedRouteSyncBatch. It adds no print, debugPrint, dump, NSLog, ad hoc logging, Logger, or sensitive-data logging.…
Cmux User-Facing Error Privacy ✅ Passed PASS. The PR changes only paired-Mac lookup logic and adds a developer comment. It adds or changes no user-facing error, alert, command output, API error body, or recovery copy. The existing debug log…
Cmux Full Internationalization ✅ Passed PASS: The PR changes only MobileShellComposite+PresenceRouteSync.swift. The diff adds a legacy pairing-ID lookup and a developer-only comment; it adds no user-facing text, localization key, catalog …
Cmux Swiftui State Layout ✅ Passed The PR changes only MobileShellComposite+PresenceRouteSync.swift, a MobileShellComposite model extension. The diff adds a local legacyPairingID lookup and passes it to applyPushedRoutes. It ad…
Cmux Architecture Rethink ✅ Passed PASS. The PR changes one existing route-sync owner by adding a tagged-ID lookup fallback to the untagged legacy pairing row. The fallback uses the existing pairedMacsByPairingID store snapshot and `…
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PASS: The PR changes only Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PresenceRouteSync.swift. The diff adds a legacy pairing-ID lookup inside presence route synchroniz…
Cmux Source Artifacts ✅ Passed The PR changes only Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PresenceRouteSync.swift. The diff adds hand-written Swift source in an existing product source directory…
Cmux No Test Or Debug Seam In Production Source ✅ Passed The only changed file is a production Swift file under Sources/, but the diff adds only a local legacyPairingID lookup, a product-behavior comment, and a fallback argument. It adds no test-build g…
Full details: Description check

Explanation

The description clearly explains the problem, implementation, scope, and test results. However, it omits the template's Demo Video, Review Trigger, and Checklist sections, including the required deterministic soak coverage status for this iOS connectivity change.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Full serial package run on air-blue (Xcode 27.0), with this PR plus #14003, #14004 and #14005 applied on db5d212d86: swift test --no-parallel finished in 119 s with 1303 tests in 114 suites. It did not hang. On main, the same run parks on presenceRoutesForHiddenDuplicateRefreshOnlyTheEmittingRow() until killed.

19 tests still fail in that run. I reran all 19 with --filter on unpatched main, and all 19 fail there too, so none are introduced by these PRs. One of them, operationFailurePreservesOpenIrohConnection, has a load-dependent number of issues: it fails with 1 or 5 depending on machine load (air-blue was at load 250-314), including with no source changes applied.

— Thimble g1 🔆

@teamleaderleo
teamleaderleo merged commit ccdbf30 into main Sep 23, 2026
51 of 52 checks passed
rustybret pushed a commit to rustybret/bmux that referenced this pull request Sep 23, 2026
ccdbf30 fix(ios): let a legacy untagged Mac row take pushed presence routes (manaflow-ai#14012)
6332063 test(ios): give pool alias fixtures one build identity (manaflow-ai#14004)
82d49a3 fix(ios): keep an offline sibling build out of secondary aggregation (manaflow-ai#14005)
19a6ac1 test: read errno before the assertion that can overwrite it (manaflow-ai#13957)
ef98b5e ci: name the macOS 26 runner variables after machines, not lanes (manaflow-ai#14009)
9697411 test(ios): expect the construction focus event in composer diagnostics (manaflow-ai#14003)
ad7f9fe ci: run app-host unit tests for a cmuxTests/ diff without a label (manaflow-ai#14017)

# Conflicts:
#	.github/workflows/ci-health-report.yml
#	.github/workflows/ci-macos.yml
#	.github/workflows/ci.yml
#	.github/workflows/nightly.yml
@teamleaderleo

Copy link
Copy Markdown
Collaborator Author

Checked under CI's Xcode 26.6: all four fixes (#14003, #14004, #14005, #14012) merged onto main db5d212d86 and run as one mobile-core-package job on hosted macos-26. A control ran plain main at the same time.

test with fixes (35888264802) main (35888268046)
mutationsEmitPrivacySafeLifecycleDiagnostics pass fail
onlineIrohAliasSelectsCurrentAuthenticatedIdentity pass fail
authenticatedIrohAliasPublishesAgainstHistoricalPresence pass fail
physicalAliasReplacementRetiresStaleAggregateSnapshots pass fail
taggedHostPortAliasOfFocusIsExcluded pass fail
focusedHandoffDrainsSubscribeBeforeFinalUnsubscribe pass fail
onlineTaggedInstanceWinsBeforePhysicalMacCoalescing pass fail
presenceRoutesForHiddenDuplicateRefreshOnlyTheEmittingRow pass never reported

With the fixes, the suite finished all 1303 tests in 386 s. Main hung and was killed at the 25-minute step timeout, so its job shows "cancelled".

21 tests still fail with the fixes. 4 of them also fail on main here: rawDeviceIDMarkerMatchingExistingRowSurvivesMigration, storedAuthorityReplacementDrainsOldControlBeforeRedial, and the two secondaryAggregationExcludes…IrohEndpoint… tests. The other 17 cannot be compared, because main hung before reaching them. The earlier full serial run on air-blue showed the same remaining failures on unpatched main.

— Nyan g1 🗝️
Run: run_cmux_main_red_suite_slices_and_errno_fixes_20260923_8053081a

teamleaderleo added a commit that referenced this pull request Sep 25, 2026
Probes older than #14012 hang in the serial CmuxMobileShell run, so tests
after the hang never execute. Counting their silence as a pass made 14
tests look broken by the commit that fixed the hang.

- Record passes as well as failures; a test that never ran at a probe is
  unknown (`-`), and a probe with no "Test run with" summary is INCOMPLETE.
- `start --patch <sha>` applies a known fix to every probe, and
  `--bisect <name>` keeps that experiment beside the first.
- Cache finished job logs, so `status --refetch` re-parses without spending
  the shared REST budget, and skip a run the API refuses instead of aborting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Sep 25, 2026
Probes older than #14012 hang in the serial CmuxMobileShell run, so tests
after the hang never execute. Counting their silence as a pass made 14
tests look broken by the commit that fixed the hang.

- Record passes as well as failures; a test that never ran at a probe is
  unknown (`-`), and a probe with no "Test run with" summary is INCOMPLETE.
- `start --patch <sha>` applies a known fix to every probe, and
  `--bisect <name>` keeps that experiment beside the first.
- Cache finished job logs, so `status --refetch` re-parses without spending
  the shared REST budget, and skip a run the API refuses instead of aborting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Sep 25, 2026
Probes older than #14012 hang in the serial CmuxMobileShell run, so tests
after the hang never execute. Counting their silence as a pass made 14
tests look broken by the commit that fixed the hang.

- Record passes as well as failures; a test that never ran at a probe is
  unknown (`-`), and a probe with no "Test run with" summary is INCOMPLETE.
- `start --patch <sha>` applies a known fix to every probe, and
  `--bisect <name>` keeps that experiment beside the first.
- Cache finished job logs, so `status --refetch` re-parses without spending
  the shared REST budget, and skip a run the API refuses instead of aborting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
teamleaderleo added a commit that referenced this pull request Sep 25, 2026
…14533)

* ci: bisect package test failures across main's history, with a skill

PR CI runs selected package tests, so the full CmuxMobileShell suite drifted
red on main without anyone noticing. Finding which PR broke each test meant
hand-building overlay branches (old commits carry CI scripts that no longer
run), dispatching test-ios.yml, scraping logs and diffing failure sets.

scripts/ci/package_bisect.py does that loop: `start` pushes probe branches
(an old commit's tree with today's iOS CI files and no lint gate) and
dispatches the package suite on runner `auto`; `status` prints a per-test
matrix with break, fix and flaky verdicts; `next` dispatches midpoints that
split each break window; `adopt` counts existing runs; `cleanup` deletes the
branches. The cmux-test-bisect skill covers when to use it, how to read the
matrix, and how to decide stale test vs regression.

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

* ci: package bisect counts only tests that ran, and can patch probes

Probes older than #14012 hang in the serial CmuxMobileShell run, so tests
after the hang never execute. Counting their silence as a pass made 14
tests look broken by the commit that fixed the hang.

- Record passes as well as failures; a test that never ran at a probe is
  unknown (`-`), and a probe with no "Test run with" summary is INCOMPLETE.
- `start --patch <sha>` applies a known fix to every probe, and
  `--bisect <name>` keeps that experiment beside the first.
- Cache finished job logs, so `status --refetch` re-parses without spending
  the shared REST budget, and skip a run the API refuses instead of aborting.

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

* ci: package bisect review fixes

- Cache job logs per run attempt, and look the attempt up first, so a rerun
  of a compile-failed probe is read fresh instead of the cached failure.
- Read a cancelled job's log: a job timeout reports as cancelled and still
  carries partial results. Only a skipped or never-started job is an error.
- Save state after every dispatch and merge probes other invocations added,
  written atomically, so a failed dispatch or a long `status --wait` cannot
  orphan branches.
- Check range endpoints against first-parent history, and space `--points`
  probes evenly instead of rounding the step down.
- `next` steps past a midpoint that answered nothing for the test instead
  of reporting the window closed.
- `adopt` keeps an existing probe's branch; `cleanup` keeps state when a
  branch deletion fails; `start --force` names the branches it leaves.
- "broken by" only names a watched commit; counts say "watched commits".
- Skill: #14510 for app-host bisects, branch naming, INCOMPLETE meaning, and
  `--patch` needs its own `--bisect` name.

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

* ci: package bisect waits on pending windows and keeps the newest probe

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

* ci: fix pending-window check in package bisect

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

* ci: run the package bisect tests in workflow-guard-tests

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

* ci: package bisect next --ways N probes a window N ways per round

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

* ci: package bisect probe subcommand adds chosen commits

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

* ci: package bisect review fixes

- SKILL.md and the docstring put --package/--bisect before the subcommand;
  the old 'start --bisect ...' form fails in argparse.
- cleanup deletes only the probe branches still on the remote, so a rerun
  after a partial cleanup finishes.
- dispatch looks the run up when 'gh workflow run' prints no URL, instead of
  leaving a probe pending forever.
- adopt checks the run's head and keeps the adopted run over a newer one.
- start rejects a reversed GOOD..BAD range; drop_lint_gate exits cleanly when
  the job is missing.
- workflow_guard_groups routes package_bisect.py to the ci guard group.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant