Guarantee bounded iOS foreground reconnect - #9256
azooz2003-bit wants to merge 5 commits into
Conversation
📝 WalkthroughWalkthroughThe PR adds route-gated and session-lifecycle diagnostics, reports cancelled attempts, improves pooled-session eviction and close attribution, and coordinates mobile recovery during foreground and presence changes. ChangesConnection recovery and diagnostics
Mobile connection admission and recovery
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Sequence Diagram(s)sequenceDiagram
participant MobileShellComposite
participant MobileRPCConnectAttemptRegistry
participant MobileCoreRPCSession
participant CmxIrohClientSessionPool
participant DiagnosticLog
MobileShellComposite->>MobileRPCConnectAttemptRegistry: request route admission
MobileRPCConnectAttemptRegistry-->>MobileCoreRPCSession: allow or return connectAttemptGated
MobileCoreRPCSession->>CmxIrohClientSessionPool: create or reuse connection
CmxIrohClientSessionPool-->>MobileCoreRPCSession: report path or close state
MobileCoreRPCSession->>DiagnosticLog: record connected, cancelled, or gated outcome
🚥 Pre-merge checks | ✅ 19 | ❌ 6❌ Failed checks (1 warning, 5 inconclusive)
✅ Passed checks (19 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 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/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileRPCTransportConnectEventTests.swift`:
- Around line 101-119: Update the test’s retry sequencing around firstTask and
waitUntilFirstConnectFinished so the retry starts only after
transportConnectObserver receives and signals the cancelled .failed event from
reportCancelledConnect. Await that real observer completion signal before
initiating the second connection, preserving the expected attempt, failed,
attempt, connected event order.
In
`@Packages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/IrohNetworkingSection.swift`:
- Around line 601-605: Add localized string-unit translations for
settings.networking.diagnostics.failure.routeGated in the existing
Localizable.xcstrings catalog, covering every supported locale and preserving
the English default value “Connection Attempt Held.”
In `@Resources/Localizable.xcstrings`:
- Around line 172910-172926: Add localized entries for
settings.networking.diagnostics.failure.routeGated in every supported catalog
locale missing from its localizations block: ar, bs, da, de, es, fr, it, km, ko,
nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, and zh-Hant. Preserve the existing en
and ja translations and use the same translated stringUnit structure for each
locale.
🪄 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 Plus
Run ID: 17477ae6-e321-4f0b-be78-ed0bbcbe674b
📒 Files selected for processing (33)
Packages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/DiagnosticReport.swiftPackages/Shared/CMUXMobileCore/Sources/CMUXMobileCore/DiagnosticTaxonomy.swiftPackages/Shared/CMUXMobileCore/Tests/CMUXMobileCoreTests/DiagnosticLogTests.swiftPackages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohClientSessionPool.swiftPackages/Shared/CmuxIrohTransport/Sources/CmuxIrohTransport/CmxIrohConnectionCloseAttribution.swiftPackages/Shared/CmuxIrohTransport/Tests/CmuxIrohTransportTests/CmxIrohClientSessionPoolPathEvictionTests.swiftPackages/Shared/CmuxIrohTransport/Tests/CmuxIrohTransportTests/CmxIrohClientSessionPoolTests.swiftPackages/Shared/CmuxIrohTransport/Tests/CmuxIrohTransportTests/CmxIrohConnectionCloseAttributionTests.swiftPackages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession+RequestSettlement.swiftPackages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileCoreRPCSession.swiftPackages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileRPCConnectAttemptRegistry.swiftPackages/iOS/CmuxMobileRPC/Sources/CmuxMobileRPC/MobileShellConnectionError.swiftPackages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileCoreRPCAbandonedConnectTests.swiftPackages/iOS/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileRPCTransportConnectEventTests.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileChatEventSource.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobilePairingFailure.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobilePresencePushRecoveryThrottle.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ConnectionRecovery.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+PresenceRouteSync.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+ReconnectRoutes.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TaskComposer.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+TaskDirectoryList.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite+WorkspaceActions.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellComposite.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/MobileShellMacAvailabilityFailureClassifier.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/SecondaryControlAttemptPolicy.swiftPackages/iOS/CmuxMobileShell/Sources/CmuxMobileShell/WorkspaceCreatePinnedContext.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobilePresencePushRecoveryThrottleTests.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellForegroundResumeTests.swiftPackages/iOS/CmuxMobileShell/Tests/CmuxMobileShellTests/MobileShellWorkspaceCreateTests.swiftPackages/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/IrohNetworkingSection.swiftResources/Localizable.xcstringsios/cmux/Resources/Localizable.xcstrings
| continuation.finish() | ||
| let recorded = await collect(events) | ||
| #expect(recorded.count == 3) | ||
| guard recorded.count == 3 else { | ||
| #expect(recorded.count == 4) | ||
| guard recorded.count == 4 else { | ||
| await session.tearDown(error: .connectionClosed) | ||
| return | ||
| } | ||
| guard case let .attempt(firstAttemptID, firstTransport) = recorded[0], | ||
| case let .attempt(secondAttemptID, secondTransport) = recorded[1], | ||
| case let .connected(connectedID, connectedTransport, _) = recorded[2] else { | ||
| Issue.record("Expected attempt, attempt, connected with no failure") | ||
| guard case let .attempt(firstAttemptID, _) = recorded[0], | ||
| case let .failed(abandonedID, abandonedTransport, abandonedFailure, _) = recorded[1], | ||
| case let .attempt(secondAttemptID, _) = recorded[2], | ||
| case let .connected(connectedID, _, _) = recorded[3] else { | ||
| Issue.record("Expected attempt, failed(cancelled), attempt, connected") | ||
| await session.tearDown(error: .connectionClosed) | ||
| return | ||
| } | ||
| #expect(firstAttemptID > 0) | ||
| #expect(secondAttemptID > 0) | ||
| #expect(firstTransport == .debugLoopback) | ||
| #expect(secondTransport == .debugLoopback) | ||
| #expect(abandonedID == firstAttemptID) | ||
| #expect(abandonedTransport == .debugLoopback) | ||
| #expect(abandonedFailure == .cancelled) | ||
| #expect(connectedID == secondAttemptID) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Wait for the cancelled event before starting the retry.
Line 103 requires the cancelled failure to precede the retry attempt. firstTask.value and waitUntilFirstConnectFinished() can complete before reportCancelledConnect() emits the failure event. The retry can then emit its attempt first.
Signal from transportConnectObserver when it receives .failed(..., .cancelled, ...). Start the retry only after that signal. As per coding guidelines, tests must await real completion signals rather than rely on scheduling order.
🤖 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/CmuxMobileRPC/Tests/CmuxMobileRPCTests/MobileRPCTransportConnectEventTests.swift`
around lines 101 - 119, Update the test’s retry sequencing around firstTask and
waitUntilFirstConnectFinished so the retry starts only after
transportConnectObserver receives and signals the cancelled .failed event from
reportCancelledConnect. Await that real observer completion signal before
initiating the second connection, preserving the expected attempt, failed,
attempt, connected event order.
Source: Coding guidelines
| case .some(.routeGated): | ||
| String( | ||
| localized: "settings.networking.diagnostics.failure.routeGated", | ||
| defaultValue: "Connection Attempt Held" | ||
| ) |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
key='settings.networking.diagnostics.failure.routeGated'
found=0
while IFS= read -r -d '' file; do
if rg -n -q "\"${key}\"" "$file"; then
found=1
printf '\n%s\n' "$file"
rg -n -C 4 "\"${key}\"" "$file"
fi
done < <(fd --type f --extension xcstrings --print0 .)
test "$found" -eq 1Repository: manaflow-ai/cmux
Length of output: 207
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
key='settings.networking.diagnostics.failure.routeGated'
printf 'Matching code references:\n'
rg -n "\"${key}\"" "$PWD" || true
printf '\nxcstrings files containing diagnostics.failure entries:\n'
while IFS= read -r -d '' file; do
if rg -q 'diagnostics\.failure' "$file"; then
printf '%s\n' "$file"
fi
done < <(find "$PWD" -type f -name '*.xcstrings' -print0 | sort -z)
printf '\nAll xcstrings entries for the key if any:\n'
python3 - <<'PY'
import json, pathlib
key='settings.networking.diagnostics.failure.routeGated'
for p in sorted(pathlib.Path('.').glob('**/*.xcstrings')):
data = json.loads(p.read_text(encoding='utf-8'))
strings = (data.get('strings') or {})
if key in strings:
loc = p.parent.name or '<root>'
print(loc, p)
entry = strings[key]
print('entry:', json.dumps(entry, ensure_ascii=False, indent=2))
else:
# search nested strings dictionaries just in case
def find_strings(d):
if isinstance(d, dict):
if 'strings' in d:
yield d['strings']
for v in d.values():
yield from find_strings(v)
hits = []
for strings in find_strings(data):
if key in strings:
hits.append(strings[key])
if hits:
print('unknown nested', p)
print(json.dumps(hits, ensure_ascii=False, indent=2))
PYRepository: manaflow-ai/cmux
Length of output: 602
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
key='settings.networking.diagnostics.failure.routeGated'
printf 'Matching code references:\n'
rg -n "\"${key}\"" "$PWD" || true
printf '\nxcstrings files containing diagnostics.failure entries:\n'
while IFS= read -r -d '' file; do
if rg -q 'diagnostics\.failure' "$file"; then
printf '%s\n' "$file"
fi
done < <(find "$PWD" -type f -name '*.xcstrings' -print0 | sort -z)
printf '\nAll xcstrings entries for the key if any:\n'
python3 - <<'PY'
import json, pathlib
key='settings.networking.diagnostics.failure.routeGated'
for p in sorted(pathlib.Path('.').glob('**/*.xcstrings')):
data = json.loads(p.read_text(encoding='utf-8'))
strings = (data.get('strings') or {})
if key in strings:
loc = p.parent.name or '<root>'
print(loc, p)
entry = strings[key]
print('entry:', json.dumps(entry, ensure_ascii=False, indent=2))
else:
def find_strings(d):
if isinstance(d, dict):
if 'strings' in d:
yield d['strings']
for v in d.values():
yield from find_strings(v)
hits = []
for strings in find_strings(data):
if key in strings:
hits.append(strings[key])
if hits:
print('unknown nested', p)
print(json.dumps(hits, ensure_ascii=False, indent=2))
PYRepository: manaflow-ai/cmux
Length of output: 602
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
python3 - <<'PY'
import json, pathlib
key='settings.networking.diagnostics.failure.routeGated'
catalog='Resources/Localizable.xcstrings'
data = json.loads(pathlib.Path(catalog).read_text(encoding='utf-8'))
entry = (data.get('strings') or {}).get(key)
if not entry:
print('Key not found in strings dictionary')
raise SystemExit(1)
print('baseValue:', entry.get('baseValue'))
translations = entry.get('translations') or {}
print('translation_key_count:', len(translations))
for lang, trans in sorted(translations.items()):
print(f'{lang}: language={trans.get("language")} region={trans.get("region")} strings={len(trans.get("stringUnit", {}).get("value", ""))} chars={len(trans.get("stringUnit", {}).get("value", ""))}')
print('\nAll non-empty translation values:')
for lang, trans in sorted(translations.items()):
value = trans.get('stringUnit', {}).get('value', '')
print(f'{lang}: {value}')
PYRepository: manaflow-ai/cmux
Length of output: 230
Add catalog translations for settings.networking.diagnostics.failure.routeGated.
Resources/Localizable.xcstrings defines the key, but it has no translations entries, so this user-facing text will only fall back to the English default. Add per-locale string-unit values for every supported locale.
🤖 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/macOS/CmuxSettingsUI/Sources/CmuxSettingsUI/Sections/IrohNetworkingSection.swift`
around lines 601 - 605, Add localized string-unit translations for
settings.networking.diagnostics.failure.routeGated in the existing
Localizable.xcstrings catalog, covering every supported locale and preserving
the English default value “Connection Attempt Held.”
Source: Path instructions
| "settings.networking.diagnostics.failure.routeGated": { | ||
| "extractionState": "manual", | ||
| "localizations": { | ||
| "en": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "Connection Attempt Held" | ||
| } | ||
| }, | ||
| "ja": { | ||
| "stringUnit": { | ||
| "state": "translated", | ||
| "value": "接続試行が一時的に保留されました" | ||
| } | ||
| } | ||
| } | ||
| }, |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
python3 - <<'PY'
import json
with open("Resources/Localizable.xcstrings", encoding="utf-8") as f:
catalog = json.load(f)
key = "settings.networking.diagnostics.failure.routeGated"
locales = catalog["strings"][key]["localizations"]
print("Locales for key:", sorted(locales))
all_locales = set()
for entry in catalog["strings"].values():
all_locales.update(entry.get("localizations", {}))
print("Catalog locales:", sorted(all_locales))
PYRepository: manaflow-ai/cmux
Length of output: 335
Add translations for all catalog locales.
settings.networking.diagnostics.failure.routeGated only defines en and ja, while Resources/Localizable.xcstrings supports ar, bs, da, de, en, es, fr, it, ja, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, and zh-Hant. Add matching translations for the missing locales to prevent fallback text.
🤖 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 `@Resources/Localizable.xcstrings` around lines 172910 - 172926, Add localized
entries for settings.networking.diagnostics.failure.routeGated in every
supported catalog locale missing from its localizations block: ar, bs, da, de,
es, fr, it, km, ko, nb, pl, pt-BR, ru, th, tr, uk, zh-Hans, and zh-Hant.
Preserve the existing en and ja translations and use the same translated
stringUnit structure for each locale.
Sources: Path instructions, Learnings
The PR reports a pending abandoned-connect cleanup as routeCleanupBlocked instead of a fake requestTimedOut. The retry loop in connectTimeoutDoesNotPoisonLaterRetryOnSameClient still only retried on requestTimedOut, so it surfaced the new error as a test failure. Retry on both, matching the updated assertions elsewhere in this file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Closeout verification (taken over from the original agent session). What this PR implements against the wake-reconnect goal
Disposition of overlapping PRs
Branch state
Test results (branch head, local)
Red/green proof (throwaway worktree of origin/main 541fe7f, only the new test file copied over)
Hosted CI (speculative merge onto main, ci.yml on
CodeRabbit findings: the two localization findings are false positives (its script read a Simulator behavioral evidence: NOT YET CAPTURED. The tagged macOS app is rebuilt, launched, socket verified, and a simulator attach ticket mints ( |
Recovery triggers (network change, presence push, liveness, dead event stream) that arrive while the iOS scene is inactive or mid-backgrounding used to dial immediately. The dial suspends with the process (field traces on the reconnect incident showed ~9.5s stalls) and later competes with the foreground recovery pass. Park the trigger in pendingInactiveRecoveryTrigger while foregroundRefreshIsActive is false and replay the most recent one exactly once in resumeForegroundRefresh(), after the foreground passes, so the replay coalesces into any attempt they already started. An explicit pairing connect and the account boundary clear the parked trigger, matching how they supersede live recovery. Green for the regression added in the previous commit. Ports the inactive-parking piece of #9256 onto connectivity v2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…imedOut When the connect-attempt registry refuses a dial because the exact route already has a connect attempt in flight (.busy), the session threw requestTimedOut. The refusal is instantaneous and never reached the network, so diagnostics recorded fabricated sub-30ms "timedOut" failures that poisoned lastFailureEvent and made exports look like the network was timing out during recovery storms. Add MobileShellConnectionError.connectAttemptGated with a dedicated DiagnosticFailureKind.routeGated (raw value 25, append-only) and throw it for the .busy gate. Callers keep their previous user-facing behavior (retryable timeout category); only the diagnostic taxonomy and the settings diagnostics rows distinguish the gate refusal. New localized strings (en/ja) for the error and both diagnostics surfaces. Single commit: the regression tests reference the new enum cases, so a tests-first commit cannot compile against main. Tests: - activeRouteAdmissionReportsRouteGatedInsteadOfTimedOut (RPC): a second session on a route with an in-flight dial gets connectAttemptGated and never allocates a transport. - gatedDialRefusalsReportRouteGatedNotTimedOut (CMUXMobileCore): a gated refusal surfaces as routeGated in lastFailureKind, never timedOut. - Taxonomy raw-value and diagnosticFailureKind mapping expectations. Ports the truth-telling piece of #9256 onto connectivity v2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…r 45s The iOS presence subscription delivers ~15s heartbeats about online Macs. While the phone is disconnected, every heartbeat restarted connection recovery through recoverFromPushedRouteBatch, so during a persistent outage the phone kept abandoning its own in-flight dials on the heartbeat cadence and each abandoned dial fed the connect-registry gate (#9177). Connectivity v2 did not absorb this: CmxConnectivityInvalidationSubscriber and ConnectivityInvalidationSubscriberCoordinator replaced the Mac-side PresenceNudgeSubscriber, while the phone-side presence path (PresenceClient -> syncPushedRoutes -> recoverFromPushedRouteBatch -> recoverMobileConnection(.presencePush)) survives unthrottled on main. MobilePresencePushRecoveryThrottle passes changed evidence (new routes, a Mac coming online) unconditionally and unchanged heartbeats at most once per 45s, above the heartbeat cadence and a recovery pass's dial budget, below the 30-60s automatic backoff ladder. Clock is injected per call (runtime?.now()); a rewound wall clock re-admits instead of freezing. Account boundary resets the throttle. Single commit: the tests reference the new type, so a tests-first commit cannot compile against main. Ports the throttle piece of #9256 onto connectivity v2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Test: recovery triggers fired while inactive must wait for the foreground probe A recovery trigger arriving while the iOS scene is inactive or mid- backgrounding must not dial: the dial suspends with the process (field traces on the reconnect incident showed ~9.5s stalls) and then competes with the foreground recovery pass. Expect no probe until resumeForegroundRefresh(), then exactly one. Red on current main; the parking fix lands in the next commit. Ports the regression from #9256 onto connectivity v2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Park inactive-phase recovery triggers and replay them on foreground Recovery triggers (network change, presence push, liveness, dead event stream) that arrive while the iOS scene is inactive or mid-backgrounding used to dial immediately. The dial suspends with the process (field traces on the reconnect incident showed ~9.5s stalls) and later competes with the foreground recovery pass. Park the trigger in pendingInactiveRecoveryTrigger while foregroundRefreshIsActive is false and replay the most recent one exactly once in resumeForegroundRefresh(), after the foreground passes, so the replay coalesces into any attempt they already started. An explicit pairing connect and the account boundary clear the parked trigger, matching how they supersede live recovery. Green for the regression added in the previous commit. Ports the inactive-parking piece of #9256 onto connectivity v2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Classify connect-registry gate refusals as connectAttemptGated, not timedOut When the connect-attempt registry refuses a dial because the exact route already has a connect attempt in flight (.busy), the session threw requestTimedOut. The refusal is instantaneous and never reached the network, so diagnostics recorded fabricated sub-30ms "timedOut" failures that poisoned lastFailureEvent and made exports look like the network was timing out during recovery storms. Add MobileShellConnectionError.connectAttemptGated with a dedicated DiagnosticFailureKind.routeGated (raw value 25, append-only) and throw it for the .busy gate. Callers keep their previous user-facing behavior (retryable timeout category); only the diagnostic taxonomy and the settings diagnostics rows distinguish the gate refusal. New localized strings (en/ja) for the error and both diagnostics surfaces. Single commit: the regression tests reference the new enum cases, so a tests-first commit cannot compile against main. Tests: - activeRouteAdmissionReportsRouteGatedInsteadOfTimedOut (RPC): a second session on a route with an in-flight dial gets connectAttemptGated and never allocates a transport. - gatedDialRefusalsReportRouteGatedNotTimedOut (CMUXMobileCore): a gated refusal surfaces as routeGated in lastFailureKind, never timedOut. - Taxonomy raw-value and diagnosticFailureKind mapping expectations. Ports the truth-telling piece of #9256 onto connectivity v2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Throttle unchanged-evidence presence-push recovery restarts to one per 45s The iOS presence subscription delivers ~15s heartbeats about online Macs. While the phone is disconnected, every heartbeat restarted connection recovery through recoverFromPushedRouteBatch, so during a persistent outage the phone kept abandoning its own in-flight dials on the heartbeat cadence and each abandoned dial fed the connect-registry gate (#9177). Connectivity v2 did not absorb this: CmxConnectivityInvalidationSubscriber and ConnectivityInvalidationSubscriberCoordinator replaced the Mac-side PresenceNudgeSubscriber, while the phone-side presence path (PresenceClient -> syncPushedRoutes -> recoverFromPushedRouteBatch -> recoverMobileConnection(.presencePush)) survives unthrottled on main. MobilePresencePushRecoveryThrottle passes changed evidence (new routes, a Mac coming online) unconditionally and unchanged heartbeats at most once per 45s, above the heartbeat cadence and a recovery pass's dial budget, below the 30-60s automatic backoff ladder. Clock is injected per call (runtime?.now()); a rewound wall clock re-admits instead of freezing. Account boundary resets the throttle. Single commit: the tests reference the new type, so a tests-first commit cannot compile against main. Ports the throttle piece of #9256 onto connectivity v2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
Superseded, all content accounted for: the wake-time dead-session validation/eviction and dial coalescing shipped with connectivity v2 on main (deadOnArrivalSessionIsClosedAndRedialedOnce, unavailableSelectedPathEvictsTheSessionAndTheNextOperationRedials); the three pieces v2 did not pick up (scene-phase recovery parking, connectAttemptGated truth-telling, presence-push recovery throttle) merged via #9347 (798aa34); the shared backoff policy merged via #9301 (c3cdbd6); the 15s all-paths-closed grace eviction is being carried into v2 by #9241. Still absent on main from this branch: cancelled-dial .cancelled reporting with the lastFailureEvent filter, and the session-level requestTimedOut→routeCleanupBlocked admission reclassification — follow-up candidates noted in 9347's PR body. |
Summary
Regression structure
CmuxIrohTransportregressions only.Test plan
swift test --package-path Packages/Shared/CmuxIrohTransportswift test --package-path Packages/iOS/CmuxMobileRPC --filter MobileCoreRPCAbandonedConnectTestsswift test --package-path Packages/iOS/CmuxMobileRPC --filter MobileRPCTransportConnectEventTestsswift test --package-path Packages/Shared/CMUXMobileCore --filter DiagnosticLogTestsswift test --package-path Packages/iOS/CmuxMobileShell --filter MobileShellForegroundResumeTestsswift test --package-path Packages/iOS/CmuxMobileShell --filter MobilePresencePushRecoveryThrottleTestsswift test --package-path Packages/iOS/CmuxMobileShell --filter MobileShellWorkspaceCreateTestsNeed help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Guarantees a bounded iOS foreground reconnect by validating pooled Iroh sessions on wake, evicting dead/no-route sessions, and deferring recovery until the app is active to avoid background bounce. Adds grace-based path-closed eviction and clearer diagnostics so recovery is reliable and easy to reason about.
New Features
CmuxIrohTransport: Foreground validation evicts closed or.unavailablesessions before first reuse; adds 15s grace eviction for “all paths closed.”CmuxMobileShell: Defers recovery triggers while inactive and replays one pending trigger on foreground; throttles presence-driven recovery (unchanged evidence at most every 45s); resets route-health gates on network changes.CmuxMobileRPC: SurfacesconnectAttemptGatedwhen a route already has an in-flight dial; reports cancelled dial outcomes to avoid false failures; drops stale gates on network changes.routeGatedfailure kind, new lifecycle events (allPathsClosed,foregroundValidationFailed), and localized strings/UI for gated attempts; updatesCmuxMobileShellUIandCmuxSettingsUIto display route-gated diagnostics.Bug Fixes
CmuxIrohTransport: Correctly attributes display-format iroh closes (“timed out”, “closed”, “closed/aborted/reset by peer …”) with prefix-anchored parsing to prevent spoofing; corpse sessions are evicted; overlapping post-wake reconnects coalesce behind one replacement dial.CMUXMobileCore: Cancelled dials no longer count as the latest failure in reports.routeCleanupBlocked.Written for commit 5cbf238. Summary will update on new commits.
Summary by CodeRabbit
New Features
Bug Fixes