Skip to content

Fix iOS deleted Iroh Mac recovery - #8683

Merged
azooz2003-bit merged 3 commits into
mainfrom
feat-account-iroh-recovery
Jul 22, 2026
Merged

azooz2003-bit merged 3 commits into
mainfrom
feat-account-iroh-recovery

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 22, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

  • Keep passive zero-touch honoring forgotten Mac markers.
  • Add explicit same-account deleted Mac recovery that scans live account-discovered Iroh Macs, filters to forgotten canonical or pairing IDs, requires Iroh routes, dials through the existing stored-Mac outcome path with the required instance tag, and clears the marker only after authenticated persistence.
  • Explain recovery in delete confirmations and show a Computers recovery action with footer copy when the current account scope has forgotten Macs.

Verification

  • swift test --filter IrohZeroTouchDiscoveryTests
  • git diff --check origin/main...HEAD
  • jq empty ios/cmux/Resources/Localizable.xcstrings
  • Touched localization keys audited for en,ja coverage.
  • CMUX_PORT=3842 CMUX_PORT_RANGE=10 CMUX_PORT_END=3851 ./scripts/reload.sh --tag irecov --launch --swift-frontend-workaround
  • CMUX_PORT=3842 CMUX_PORT_RANGE=10 CMUX_PORT_END=3851 ./ios/scripts/reload.sh --tag irecov --simulator cmux-irecov-0722
  • CMUX_PORT=3842 CMUX_PORT_RANGE=10 CMUX_PORT_END=3851 ./scripts/mobile-dev-launch.sh --tag irecov --simulator cmux-irecov-0722 --ensure-mac
  • Warmed http://127.0.0.1:3842/, /handler/sign-in, and /handler/after-sign-in.

Dogfood

Tagged macOS build: http://127.0.0.1:17320/irecov
iOS tag: irecov on simulator cmux-irecov-0722 (0A967C23-FCCE-4140-B0D9-EF9A4C63A0F8).


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Enables explicit recovery of deleted Iroh Macs on iOS via same-account discovery. Adds a Recover Deleted Computer action and keeps zero-touch discovery from auto-reviving forgotten Macs.

  • New Features
    • Recovery flow: discover live same-account Iroh Macs, filter to forgotten canonical/pairing IDs, require .iroh routes and matching instance tag, accept mixed-route candidates only when .iroh exists (persist .iroh), connect via stored-Mac path, and clear the marker only after authenticated save.
    • UI: show “Recover Deleted Computer” with a helper footer; progress indicator and failure alert; cancel in-flight recovery when leaving the screen; clearer delete confirmation copy.
    • State: track hasRecoverableDeletedComputers per scope, set on paired‑Mac load, reset on sign-out and data reload; added connectAccountDiscoveredIrohMac.
    • Tests & i18n: added success, mixed-route acceptance, and failure tests; new en/ja strings for the recover button, footer, progress, and alert copy.

Written for commit 1f78f1c. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Added “Recover Deleted Computer” to restore previously removed Macs from a live discovery on the same account.
    • Recovery automatically reconnects eligible Macs, then reloads paired devices.
    • Shows a “recovering” progress state and presents a failure alert when recovery can’t be completed.
    • Recovery availability and status are reset on sign-out and when switching teams.
  • Documentation

    • Updated English and Japanese delete/removal copy and added new localized strings for the recovery flow.
  • Tests

    • Added/expanded tests covering successful recovery, failed recovery (marker preserved), and mixed eligible-route scenarios.

@coderabbitai

coderabbitai Bot commented Jul 22, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Deleted Computer Recovery

Layer / File(s) Summary
Recovery state and connection flow
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/...
Tracks recoverable deleted computers, matches forgotten markers to discovered Iroh Macs, and reconnects them using the advertised instance tag.
Recovery success and failure validation
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/IrohZeroTouchDiscoveryTests.swift
Verifies successful recovery persistence, mixed-route handling, marker removal, and failure behavior that preserves the forgotten marker.
Recovery controls and messaging
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/..., ios/cmux/Resources/Localizable.xcstrings
Adds recovery controls, progress and cancellation state, failure alerts, accessibility labeling, and English/Japanese recovery copy.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DeviceTreeView
  participant MobileShellComposite
  participant IrohDiscovery
  participant ConnectionRecovery
  participant PairedMacStore
  DeviceTreeView->>MobileShellComposite: Start deleted-computer recovery
  MobileShellComposite->>IrohDiscovery: Discover live account Macs
  IrohDiscovery-->>MobileShellComposite: Return forgotten Mac candidates
  MobileShellComposite->>ConnectionRecovery: Connect with instance tag
  ConnectionRecovery-->>MobileShellComposite: Return connection result
  MobileShellComposite->>PairedMacStore: Load paired Macs and registry devices
  MobileShellComposite-->>DeviceTreeView: Return recovery result
Loading
🚥 Pre-merge checks | ✅ 25
✅ Passed checks (25 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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 Swift Actor Isolation ✅ Passed The new recovery code stays on @MainActor, and the UI Task is explicitly @MainActor; no new shared-mutable Sendable types or background store access were introduced.
Cmux Swift Blocking Runtime ✅ Passed The patch only adds async Task/cancellation state and recovery UI; the commit diff introduces no semaphores, sleeps, sync dispatch, locks, or polling waits.
Cmux Browser Automation Off-Main ✅ Passed PR only changes iOS Mac recovery/UI/localization files; no browser socket automation paths or socket-worker routing code were touched.
Cmux Expensive Synchronous Load ✅ Passed Touched files add only async Iroh recovery/UI wiring; no RestorableAgentSessionIndex, agent-history scans, JSONL parsing, or other sync heavy loaders were introduced.
Cmux Cache Substitution Correctness ✅ Passed The new cache-backed flag is only a UI hint, while recovery still rechecks current scope and store-backed forgotten IDs with cold-cache fallback.
Cmux No Hacky Sleeps ✅ Passed PASS: PR only changes Swift sources, tests, and xcstrings; no TypeScript/JavaScript/shell/build-runtime files or fixed-wait patterns were introduced.
Cmux Algorithmic Complexity ✅ Passed PASS: New recovery scans are single-pass with a hard cap of 4, and lookups use sets; no nested rescans or hot-path sorting/filtering were introduced.
Cmux Swift Concurrency ✅ Passed No new legacy async pattern is introduced; the only new Task is stored and canceled, and the rest uses async/await across SwiftUI/AppKit boundaries.
Cmux Swift @Concurrent ✅ Passed No new nonisolated async/@Concurrent mismatch; the added recovery flow stays intentionally @MainActor and mirrors existing UI-bound zero-touch logic.
Cmux Swift Package Boundaries ✅ Passed All changed Swift code lives under SwiftPM package targets (CmuxMobileShell/CmuxMobileShellUI); the app target only changes localization resources, so no boundary violation.
Cmux Swiftpm Lockfiles ✅ Passed No Package.swift, Package.resolved, .gitignore, or Xcode project changes appear in the diff, so the SwiftPM lockfile policy is not violated.
Cmux Swift Logging ✅ Passed Diff adds no print/debugPrint/dump/NSLog or Logger changes in app/runtime Swift code; only recovery logic, tests, and copy updates.
Cmux User-Facing Error Privacy ✅ Passed New recovery alert/footer copy stays product-level and doesn’t expose upstream names, raw errors, or internal IDs.
Cmux Full Internationalization ✅ Passed All new UI copy uses L10n.string (a String(localized:) wrapper) and the touched catalog has complete en/ja translations for every added/changed key.
Cmux Swiftui State Layout ✅ Passed No forbidden SwiftUI pattern appears: DeviceTreeView uses local @State only, rows take snapshots/closures, and no GeometryReader/ObservableObject regression was added.
Cmux Architecture Rethink ✅ Passed PASS: Recoverability stays owned by MobileShellComposite, derived from forgottenMacDeviceIDs and reset on sign-out/team change; UI only holds transient task/alert state, with no new timing/observer...
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed Diff only adds iOS navigation/list/form/alert UI and shell logic; no NSWindow/NSPanel/WindowGroup code or cmuxAuxiliaryWindowIdentifiers changes.
Cmux Source Artifacts ✅ Passed Full PR diff only changes source, tests, UI, and .xcstrings localization files; no artifact-style paths matched the review rule.
Cmux No Test Or Debug Seam In Production Source ✅ Passed PASS: the changed production Sources add real recovery UI/state, with no new #if DEBUG or test-only seam names; tests read the internal state via @testable import.
Cmux No Ambient Global State ✅ Passed PASS: the new recovery logic and flags live on MobileShellComposite/DeviceTreeView; no new file-scope funcs, mutable globals, or singleton namespaces were added.
Title check ✅ Passed The title clearly summarizes the main change: iOS recovery for deleted Iroh Macs.
Description check ✅ Passed The description includes a solid summary and verification section and is mostly complete, though some template sections are omitted.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-account-iroh-recovery

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.

@greptile-apps

greptile-apps Bot commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds explicit user-initiated recovery of deleted (forgotten) Iroh Macs on iOS. Passive zero-touch discovery continues to skip forgotten Macs; this new path is the only way to bring one back without re-pairing.

  • New recovery flow (MobileShellComposite+ForgottenMacRecovery.swift): scans live same-account Iroh broker candidates, filters to forgotten canonical/pairing IDs via forgottenIrohRecoveryCandidates, then dials through connectAccountDiscoveredIrohMac (which enforces instance-tag authentication). The forgotten marker is only cleared after connectStoredMacOutcome persists the record.
  • UI (DeviceTreeView): shows a "Recover Deleted Computer" section with progress state, failure alert, task stored for onDisappear cancellation, and a recoveryAttemptID guard against stale task completions. hasRecoverableDeletedComputers is a new public internal(set) flag on MobileShellComposite that drives section visibility, reset on sign-out and team-scope reload.
  • Localization: five new keys with complete en/ja coverage; two existing delete-confirmation keys updated in both locales.

Confidence Score: 4/5

Safe to merge with one fix: the connection recovery owner is cancelled before live candidates are confirmed, and a zero-candidates failure leaves any in-progress Mac reconnect silently dropped.

The recovery flow, instance-tag enforcement, forgotten-marker lifecycle, and localization are all correctly implemented. The one concrete issue is in recoverForgottenIrohMacFromAccount: connectionRecoveryOwner.cancel() + invalidateStoredMacReconnectAttempt() fire before discoverLiveMacs() is called. If no live candidates match the forgotten ID, the function returns false having already abandoned any in-progress reconnect to an existing (non-forgotten) Mac. Natural re-arming via network-path changes or presence pushes will eventually restore it, but the user sees their primary Mac drop from reconnecting to disconnected as a side effect of tapping a button that found nothing to recover.

Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ForgottenMacRecovery.swift — the early cancel/invalidate before candidates are confirmed.

Important Files Changed

Filename Overview
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ForgottenMacRecovery.swift New explicit forgotten-Mac recovery logic. Route guard (contains(.iroh)) aligns with connectAccountDiscoveredIrohMac. Key issue: connectionRecoveryOwner is cancelled before live candidates are confirmed, disrupting in-progress reconnects when no candidates are found.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ConnectionRecovery.swift Adds connectAccountDiscoveredIrohMac which correctly routes live Iroh-discovered Macs through connectStoredMacOutcome with an instance-tag requirement. Route filtering uses the same contains(.iroh) guard as the recovery candidates filter.
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift Adds hasRecoverableDeletedComputers flag, correctly reset in sign-out, team-scope reload, and early-return paths of loadPairedMacs(). Set to true only after forgottenMacDeviceIDs confirms non-empty forgotten set.
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift Adds recovery section with progress state, failure alert, and onDisappear cancellation. Task is stored and cancelled on disappear. An existing thread already flags the unstructured Task{} lifetime concern for this view.
ios/cmux/Resources/Localizable.xcstrings Adds 5 new keys (recoverDeleted, recoverDeletedFooter, recoverFailedMessage, recoverFailedTitle, recoveringDeleted) and updates 2 existing keys. All entries have complete en+ja translations.
Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/IrohZeroTouchDiscoveryTests.swift Adds three new tests: success path (dials forgotten Mac, clears marker), mixed-route candidate (Iroh+Tailscale, verifies only Iroh route saved), and failure path (marker preserved, no persisted record).

Sequence Diagram

sequenceDiagram
    participant UI as DeviceTreeView
    participant Shell as MobileShellComposite
    participant Discovery as PersonalIrohDiscovery

    UI->>Shell: recoverForgottenIrohMacFromAccount()
    Shell->>Shell: forgottenMacDeviceIDs(scope)
    Shell->>Shell: connectionRecoveryOwner.cancel()
    Shell->>Shell: invalidateStoredMacReconnectAttempt()
    Shell->>Discovery: discoverLiveMacs()
    Discovery-->>Shell: [MobileDiscoveredIrohMac]
    Shell->>Shell: forgottenIrohRecoveryCandidates(discovered, forgottenIDs)
    loop For each candidate
        Shell->>Shell: isForgottenMacDeviceID(mac, scope)
        Shell->>Shell: connectAccountDiscoveredIrohMac(mac, accountID)
        alt Connected and authenticated
            Shell->>Shell: "loadPairedMacs() → hasRecoverableDeletedComputers=false"
            Shell->>Shell: loadRegistryDevices()
            Shell-->>UI: true
        else Failed
            Shell->>Shell: continue to next candidate
        end
    end
    Shell-->>UI: false
    UI->>Shell: reload()
    UI->>UI: show failure alert or dismiss spinner
Loading

Reviews (2): Last reviewed commit: "fix(ios): tighten deleted Mac recovery r..." | Re-trigger Greptile

Comment on lines +80 to +84
guard forgottenIDs.contains(cmxCanonicalDeviceID(mac.deviceID))
|| forgottenIDs.contains(pairingID),
!mac.routes.isEmpty,
mac.routes.allSatisfy({ $0.kind == .iroh }),
seen.insert(pairingID).inserted else { continue }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 allSatisfy(.iroh) route guard is stricter than connectAccountDiscoveredIrohMac's acceptance check

forgottenIrohRecoveryCandidates requires every route to be .iroh, but connectAccountDiscoveredIrohMac only requires at least one Iroh route after storedReconnectRoutes filtering (contains(where: { $0.kind == .iroh })). The fact that connectAccountDiscoveredIrohMac passes mac.routes through storedReconnectRoutes at all implies MobileDiscoveredIrohMac can carry mixed-kind routes. A live forgotten Mac that advertises both an Iroh route and a direct/loopback route would pass connectAccountDiscoveredIrohMac's gate but be dropped here, causing the recovery to silently return false even though a valid Iroh path exists. Changing the guard to mac.routes.contains(where: { $0.kind == .iroh }) aligns the two checks.

Comment on lines +247 to +258
Task {
let recovered = await store.recoverForgottenIrohMacFromAccount()
await reload()
isRecoveringDeletedComputer = false
if !recovered {
recoveryAlertMessage = L10n.string(
"mobile.computers.recoverFailedMessage",
defaultValue: "No deleted computer was recovered. Open cmux on the Mac, sign in to this same account, and try again."
)
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unstructured Task {} outlives the view and fires connection-state side effects after dismissal

recoverDeletedComputer() launches an unstructured Task that is not tied to the view's lifetime. If the user dismisses the Computers screen while recovery is in progress, recoverForgottenIrohMacFromAccount() continues running: it has already cancelled connectionRecoveryOwner and may complete a full Iroh dial and persist a new Mac record. The resulting reload() (which calls loadPairedMacs() and loadRegistryDevices()) also fires into the dismissed view's store. None of this crashes, but the user sees no feedback and may be surprised that their active reconnect was silently cancelled by a navigation gesture. Storing the task handle and cancelling it onDisappear, or switching to .task(id:) on a trigger value, would scope the side-effectful work to the screen's visible lifetime.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+ForgottenMacRecovery.swift:
- Around line 47-51: Update isForgottenMacDeviceID to canonicalize the supplied
device ID before performing its direct lookup, matching the normalization used
during candidate selection and preserving legacy marker handling. Add a
regression test covering an uppercase UUID with a legacy canonical marker,
verifying the forgotten-device revalidation succeeds.
- Around line 80-84: Update the candidate filter in the forgotten-Mac recovery
flow to accept Macs with at least one Iroh route, rather than requiring every
route to be Iroh. Preserve the existing non-empty route check, forgotten-ID
matching, and duplicate suppression via seen.insert(pairingID), while ensuring
candidates without any Iroh route remain excluded.
- Around line 55-59: Update the recovery flow surrounding the ifStillCurrent
closure to capture secondaryAggregationScopeGeneration before discovery, then
require the current generation to match the captured value alongside the
existing sign-in and user-ID checks. Ensure recovery returns false when the team
scope changes while the dial is awaiting, preventing the stale scope from
connecting or persisting.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift`:
- Around line 248-250: Remove the unconditional reload() call after
recoverForgottenIrohMacFromAccount() succeeds in the recovery flow, since that
method already refreshes paired Macs and registry devices. Preserve the
isRecoveringDeletedComputer state reset and only retain a conditional reload if
a specific result path requires it.
- Around line 184-186: Update the recovery and deletion messaging to clearly
identify this phone as the recovery-action surface: revise the fallback strings
in DeviceTreeView, MacComputerDetailView, and both MacComputerRow fallbacks,
plus the corresponding English and catalog entries for
mobile.computers.removeMessage,
mobile.computers.removeMessageRepresentativeFormat, and
mobile.computers.recoverDeletedFooter in
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift
(184-186),
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerDetailView.swift
(247),
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerRow.swift
(176-181), and ios/cmux/Resources/Localizable.xcstrings (1506-1512, 1523-1529,
1574-1580); add “on this phone” or equivalent “here” wording while preserving
the existing instructions.
- Around line 243-257: Update recoverDeletedComputer to capture the
authoritative current scope, store the recovery Task for cancellation, and
cancel it when the view lifecycle or scope changes. Before applying reload
results or mutating isRecoveringDeletedComputer and recoveryAlertMessage, verify
the task and scope are still current; otherwise discard the result. Treat
recovered == false as a failure only for the current scope.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8c56afd6-06ba-46e0-a796-9076b41eb997

📥 Commits

Reviewing files that changed from the base of the PR and between 7b4f830 and 68ce8ba.

📒 Files selected for processing (8)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ConnectionRecovery.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ForgottenMacRecovery.swift
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/IrohZeroTouchDiscoveryTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerDetailView.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerRow.swift
  • ios/cmux/Resources/Localizable.xcstrings

Comment on lines +47 to +51
guard await isForgottenMacDeviceID(
mac.deviceID,
instanceTag: mac.instanceTag,
scope: scope
) else { continue }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Canonicalize the forgotten-ID revalidation.

Candidate selection canonicalizes mac.deviceID, but isForgottenMacDeviceID first compares the raw ID. A legacy canonical marker and differently cased UUID can be selected at Line 80, then rejected here. Normalize the direct device-ID lookup in isForgottenMacDeviceID and add an uppercase-UUID recovery regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+ForgottenMacRecovery.swift
around lines 47 - 51, Update isForgottenMacDeviceID to canonicalize the supplied
device ID before performing its direct lookup, matching the normalization used
during candidate selection and preserving legacy marker handling. Add a
regression test covering an uppercase UUID with a legacy canonical marker,
verifying the forgotten-device revalidation succeeds.

Comment on lines +55 to +59
ifStillCurrent: { [weak self] in
guard let self else { return false }
return self.isSignedIn
&& self.identityProvider?.currentUserID == scope.userID
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep the dial bound to the captured team scope.

This guard only validates the account. If the team changes while the dial awaits, the old-scope recovery can still connect and persist. Capture secondaryAggregationScopeGeneration before discovery and require it here so the attempt fails closed after a scope transition.

Proposed fix
+        let recoveryScopeGeneration = secondaryAggregationScopeGeneration
         let forgottenIDs = await forgottenMacDeviceIDs(scope: scope)
...
                     return self.isSignedIn
                         && self.identityProvider?.currentUserID == scope.userID
+                        && self.secondaryAggregationScopeGeneration == recoveryScopeGeneration

As per path instructions, recovery eligibility and scope/team identity must come from authoritative scope data and fail closed when it changes.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ifStillCurrent: { [weak self] in
guard let self else { return false }
return self.isSignedIn
&& self.identityProvider?.currentUserID == scope.userID
}
ifStillCurrent: { [weak self] in
guard let self else { return false }
return self.isSignedIn
&& self.identityProvider?.currentUserID == scope.userID
&& self.secondaryAggregationScopeGeneration == recoveryScopeGeneration
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+ForgottenMacRecovery.swift
around lines 55 - 59, Update the recovery flow surrounding the ifStillCurrent
closure to capture secondaryAggregationScopeGeneration before discovery, then
require the current generation to match the captured value alongside the
existing sign-in and user-ID checks. Ensure recovery returns false when the team
scope changes while the dial is awaiting, preventing the stale scope from
connecting or persisting.

Source: Path instructions

Comment on lines +184 to +186
Text(L10n.string(
"mobile.computers.recoverDeletedFooter",
defaultValue: "Deleted computers stay hidden on this phone. To recover one, open cmux on that Mac, sign in to this same account, then tap Recover Deleted Computer."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Identify the phone as the recovery-action surface.

The shared wording tells users to open cmux on the Mac and then tap “Recover Deleted Computer,” which can imply that the tap happens in the Mac app. Add “on this phone”/“here” to the fallback strings and both catalog locales.

  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift#L184-L186: update the recovery footer.
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerDetailView.swift#L247-L247: update the detail-view fallback.
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerRow.swift#L176-L181: update both row fallbacks.
  • ios/cmux/Resources/Localizable.xcstrings#L1506-L1512: update mobile.computers.removeMessage.
  • ios/cmux/Resources/Localizable.xcstrings#L1523-L1529: update mobile.computers.removeMessageRepresentativeFormat.
  • ios/cmux/Resources/Localizable.xcstrings#L1574-L1580: update mobile.computers.recoverDeletedFooter.
📍 Affects 4 files
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift#L184-L186 (this comment)
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerDetailView.swift#L247-L247
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerRow.swift#L176-L181
  • ios/cmux/Resources/Localizable.xcstrings#L1506-L1512
  • ios/cmux/Resources/Localizable.xcstrings#L1523-L1529
  • ios/cmux/Resources/Localizable.xcstrings#L1574-L1580
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift`
around lines 184 - 186, Update the recovery and deletion messaging to clearly
identify this phone as the recovery-action surface: revise the fallback strings
in DeviceTreeView, MacComputerDetailView, and both MacComputerRow fallbacks,
plus the corresponding English and catalog entries for
mobile.computers.removeMessage,
mobile.computers.removeMessageRepresentativeFormat, and
mobile.computers.recoverDeletedFooter in
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift
(184-186),
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerDetailView.swift
(247),
Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/MacComputerRow.swift
(176-181), and ios/cmux/Resources/Localizable.xcstrings (1506-1512, 1523-1529,
1574-1580); add “on this phone” or equivalent “here” wording while preserving
the existing instructions.

@cursor

cursor Bot commented Jul 22, 2026

Copy link
Copy Markdown

Bugbot is paused — on-demand spend limit reached

Bugbot uses usage-based billing for this team and has hit its on-demand spend limit.

A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ForgottenMacRecovery.swift (1)

26-47: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Propagate Task cancellation through forgotten Mac recovery.

recoverForgottenIrohMacFromAccount() never checks Task.isCancelled, and the closure passed into connectAccountDiscoveredIrohMac(...) doesn’t either. After cancelling the UI task, discovery/re-selection can still dial and persist a pairing, then remove the forgotten marker. Add a cancellation guard after each suspension point and include !Task.isCancelled in ifStillCurrent.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+ForgottenMacRecovery.swift
around lines 26 - 47, The forgotten-Mac recovery flow in
recoverForgottenIrohMacFromAccount must stop after cancellation: add
Task.isCancelled guards immediately after each awaited suspension point before
continuing discovery, candidate selection, validation, or recovery. Update the
ifStillCurrent closure passed to connectAccountDiscoveredIrohMac to also require
!Task.isCancelled, preventing dialing, persistence, and forgotten-marker removal
after the task is cancelled.

Sources: Coding guidelines, Path instructions

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In
`@Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite`+ForgottenMacRecovery.swift:
- Around line 26-47: The forgotten-Mac recovery flow in
recoverForgottenIrohMacFromAccount must stop after cancellation: add
Task.isCancelled guards immediately after each awaited suspension point before
continuing discovery, candidate selection, validation, or recovery. Update the
ifStillCurrent closure passed to connectAccountDiscoveredIrohMac to also require
!Task.isCancelled, preventing dialing, persistence, and forgotten-marker removal
after the task is cancelled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c9cd6dab-adf3-410e-9d03-fc1d2faf41d6

📥 Commits

Reviewing files that changed from the base of the PR and between 68ce8ba and 1f78f1c.

📒 Files selected for processing (3)
  • Packages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ForgottenMacRecovery.swift
  • Packages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/IrohZeroTouchDiscoveryTests.swift
  • Packages/iOS/CmuxMobileShellUI/Sources/CmuxMobileShellUI/DeviceTreeView.swift

Comment on lines +22 to +31
connectionRecoveryOwner.cancel()
applyConnectionRecoveryOwnerState()
invalidateStoredMacReconnectAttempt()

let discovered = await personalIrohDiscovery.discoverLiveMacs()
guard await isScopeCurrent(scope) else { return false }
let candidates = forgottenIrohRecoveryCandidates(
from: discovered,
forgottenIDs: forgottenIDs
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 connectionRecoveryOwner cancelled before candidates exist

connectionRecoveryOwner.cancel() + invalidateStoredMacReconnectAttempt() fire before discoverLiveMacs() is even called. If discovery returns no matching candidates (Mac B is offline or was truly deleted), the function returns false with no rollback — any in-progress connectionRecoveryOwner attempt for a non-forgotten Mac (Mac A momentarily dropped its connection while Mac B's forgotten record persists) is silently abandoned. The next natural re-arm comes from a network-path change or presence push, but the disruption is observable: the UI goes from "reconnecting" to "disconnected" even though the user's primary Mac is still reachable.

Moving the three-line cancel block to after guard !candidates.isEmpty (or after the first connectAccountDiscoveredIrohMac is determined to be worth calling) scopes the slot takeover to when a real connection attempt is imminent, which matches the beginPairingAttempt() pattern everywhere else.

@azooz2003-bit
azooz2003-bit merged commit 70224d2 into main Jul 22, 2026
9 checks passed
@azooz2003-bit
azooz2003-bit deleted the feat-account-iroh-recovery branch July 22, 2026 22:43
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