Skip to content

fix(mobile): do not call a suspended dial an unreachable host - #24425

Open
innocarpe wants to merge 3 commits into
stablyai:mainfrom
innocarpe:fix-18829-suspended-dial-timeout
Open

innocarpe wants to merge 3 commits into
stablyai:mainfrom
innocarpe:fix-18829-suspended-dial-timeout

Conversation

@innocarpe

Copy link
Copy Markdown
Contributor

Description

On iOS, suspending Orca Mobile freezes setTimeout. A connect timer armed for 12 seconds can fire hours later, while the socket is still connecting, and the diagnostics log says No TCP/WS handshake within 12s — endpoint unreachable? about a host that was up the whole time. The handshake timer has the same shape.

Focused fix

In scope: if wall-clock time from arming the timer to the callback is more than the timeout plus 60 seconds, log a suspension (suspended-dial) and still close the socket so the existing reconnect runs. The same reading covers the connect timer and the handshake timer.

Out of scope: the stale Host Orca version on the diagnostics screen, liveness probes, and any change to how long a live timeout waits.

Preserves

A timer that fires on its budget, or up to 60 seconds late, still reports connect-timeout / handshake-timeout with the existing unreachable and handshake text. User-initiated reconnect is unchanged. The frozen socket is still closed.

Evidence

node node_modules/vitest/vitest.mjs run --config mobile/vitest.config.ts --cache false src/transport/socket-deadline-suspension.test.ts src/diagnostics/connection-diagnostics-analysis.test.ts

21 passed. The 2h44m gap from the report is classified as App suspended for 2h44m; connection state unknown, re-dialing, and that entry is not diagnosed as an endpoint that missed its timeout. An iOS device was not rechecked.

User-regression-tradeoffs

A connect or handshake timer that fires more than a minute late is no longer described as an unreachable host. A genuinely dead endpoint that answers the timer on time is still reported as unreachable.

Fixes #18829

@greptile-apps

greptile-apps Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Distinguishes app suspension from unreachable hosts in connection diagnostics.

The PR should not merge until an on-budget timeout remains diagnosable after a brief inactive transition.

Findings

  1. P1 Brief inactivity hides real timeouts ▶

Summary

The PR distinguishes delayed mobile socket deadlines from ordinary connect and handshake timeouts, records suspended dials separately, and keeps forced-close recovery. The new clock also checks foreground transitions and monotonic elapsed time to avoid treating wall-clock jumps as suspension.

Reviews (2) · Last reviewed commit: "fix(mobile): do not treat a wall-clock j..."

Comment thread mobile/src/transport/socket-deadline-suspension.ts Outdated
@coderabbitai

coderabbitai Bot commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

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

🧰 Additional context used
📚 Code guidelines (1)
AGENTS.md — auto-discovered

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: af895404-8803-4854-8aa8-f5d23a4fa1b6
📥 Commits

Reviewing files that changed from the base of the PR and between 6e08f2949a3ce92454ddfa1cb7c195f06e3481a2 and d2c52a0.

📒 Files selected for processing (4)
  • mobile/src/transport/socket-deadline-clock.test.ts
  • mobile/src/transport/socket-deadline-clock.ts
  • mobile/src/transport/socket-deadline-suspension.test.ts
  • mobile/src/transport/socket-deadline-suspension.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Walkthrough

Walkthrough

Connect and handshake deadlines use shared reporting that distinguishes timeouts from deadlines delayed beyond the timeout plus 60 seconds. The deadline clock records elapsed wall and monotonic time and tracks app foreground loss. Suspended deadlines produce warning-level suspended-dial diagnostics and close the connection. Tests cover deadline classification, reporting, timer cleanup, and diagnosis of suspended-dial warnings.

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to d2c52

No confirmed issue blocks merging; ordinary timeouts retain their previous behavior and text.

Security Architecture Review

Security architecture risk: 🔵 Low · up to d2c52

The change separates delayed connection diagnostics from genuine timeouts while preserving socket closure and authentication checks. No expanded access or weakened authentication was identified. Remaining uncertainty concerns native suspension behavior, which was not rechecked on an iOS device.

Retained concerns
No architecture-level concerns identified.

Security review details

Security Blast Radius

  • inferred — The evidenced exposure is the mobile client's current connection attempt and its diagnostic output. The helpers receive local timing samples and caller-supplied callbacks; they do not acquire additional socket, credential, or service authority.

Security Findings and Attack Paths

  • inferred — A peer that withholds connection or authentication progress can still cause deadline-driven closure, as before. Suspension classification changes the diagnostic severity and description, but does not make that pending connection authenticated or exempt it from closure.

Trust Boundaries and Controls

  • observed — Forced-close authority remains with the existing socket-close controller. It checks current-session identity, clears timers and keys, detaches the session, rejects interrupted requests, and schedules recovery. Deadline classification does not replace these controls.

Resilience and Maintainability Implications

  • observed — Explicit client close releases deadline timers before closing the session. Stale deadline callbacks consume their own clock association and return without reporting or closing the replacement socket, preserving connection ownership during recovery.
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning Issue #18829 has two coding objectives. The PR implements suspension handling for connect and handshake deadlines: it classifies delayed deadlines, logs suspended-dial, closes the socket, and tests … Update the diagnostics display or report path to omit the cached Host Orca version when disconnected, or label it with the last successful connection time. Add automated tests for the chosen behavior.
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 10 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: preventing suspended mobile connections from being misdiagnosed as unreachable hosts.
Description check ✅ Passed The description explains the problem, the focused fix, preserved behavior, scope, linked issue, and test results. It does not include every template section, such as an explicit visual-proof statement…
Out of Scope Changes check ✅ Passed The production changes, diagnostic classification, and tests support the suspension-handling objective in issue #18829. No unrelated change is demonstrated.
Full details: Linked Issues check

Explanation

Issue #18829 has two coding objectives. The PR implements suspension handling for connect and handshake deadlines: it classifies delayed deadlines, logs suspended-dial, closes the socket, and tests the diagnostic classification. It does not resolve the stale Host Orca version objective. The changed files do not update the version display or test omission or timestamp labeling. The current diagnostics snapshot still reads cached lastConnectedAt.

  • Fix all pre-merge checks with AI
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


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.

@pullfrog pullfrog Bot left a comment

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.

✅ No new issues found.

Reviewed changes

  • Extracted suspension classifier — mobile/src/transport/socket-deadline-suspension.ts (new) keys off wall-clock elapsed: a deadline that fires more than timeout + 60s past arm is reported as suspended-dial (warn) instead of the host-blame connect-timeout / handshake-timeout (error), while still closing the socket.
  • Session refactor — armConnectTimeout / armHandshakeTimeout collapse into armSocketDeadline in rpc-client-socket-session.ts; the pending predicate is equivalent to the old inline guards for both kinds, and timer assignment/clearing is preserved.
  • Diagnostics — suspended-dial is added to ConnectionDiagnosticCode but intentionally left out of isDiagnosticFailure and every diagnoseFailure branch, so a suspended dial is never reported as an unreachable host.
  • Tests — new socket-deadline-suspension.test.ts covers both kinds, the exact timeout + slack boundary versus +1, and that close still runs; the diagnostics test pins that a lone suspended entry yields the no-single-cause fallback.

The refactor is behavior-preserving for the on-time and mildly-late paths, and the new code is scoped to the suspension case. The documented tradeoffs (a host whose timer fires >60s late, or a forward clock jump) are acceptable for this focused fix.

<!--
Pullfrog review metadata. These findings were written against bdf7d3be80;
if commits have landed on fix-18829-suspended-dial-timeout since, treat every specific bug, file, or
line callout as POTENTIALLY STALE and re-diff before acting on it.

- Mode: Review (initial)
- Files reviewed: 5
- Commits reviewed: 1
- Base: main (449b8ca17d)
- Head: fix-18829-suspended-dial-timeout (bdf7d3be80)
- Reviewed commits:
  - bdf7d3be80 — fix(mobile): do not call a suspended dial an unreachable host
- Prior pullfrog review: none
-->

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@innocarpe
innocarpe force-pushed the fix-18829-suspended-dial-timeout branch from bdf7d3b to 6e08f29 Compare October 1, 2026 22:55
@innocarpe

Copy link
Copy Markdown
Contributor Author

Sync update (6e08f2949a)

A wall-clock jump no longer reads as suspension unless the monotonic clock also ran long or the app left the foreground. Leaving the foreground is still suspension when the wall clock jumps backward. Eleven deadline tests passed. This does not cover a device sleep on iOS.

Comment thread mobile/src/transport/socket-deadline-suspension.ts

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 9cd66170-7e1a-4ca6-8b79-be9d2fcefa52

📥 Commits

Reviewing files that changed from the base of the PR and between bdf7d3be8011136f30583f3ca1af327073eb908b and 6e08f2949a3ce92454ddfa1cb7c195f06e3481a2.

📒 Files selected for processing (5)
  • mobile/src/transport/rpc-client-socket-session.ts
  • mobile/src/transport/socket-deadline-clock.test.ts
  • mobile/src/transport/socket-deadline-clock.ts
  • mobile/src/transport/socket-deadline-suspension.test.ts
  • mobile/src/transport/socket-deadline-suspension.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

Comment thread mobile/src/transport/socket-deadline-clock.ts

@pullfrog pullfrog Bot left a comment

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.

Important

A transient foreground flicker now permanently marks a deadline as suspended, so an on-budget timer can be reported as suspended-dial and a genuine host failure drops out of diagnostics.

Reviewed changes

  • Added a deadline clock — new mobile/src/transport/socket-deadline-clock.ts records wall arm time, a performance.now() monotonic elapsed, and a sticky leftForeground flag driven by AppState change.
  • Extended the classifier — describeSocketDeadline now combines wall elapsed, monotonic elapsed, and leftForeground, so a forward wall-clock jump stays a host timeout while a foreground loss reads as suspension.
  • Rewired the session — armSocketDeadline captures the clock sample at fire and clears timers through releaseSocketTimer (which also removes the AppState listener), replacing the bare clearTimeout calls.
  • Tests — new socket-deadline-clock.test.ts plus four socket-deadline-suspension.test.ts cases cover the jump, backward jump, both-clocks-long, and listener cleanup.

One concern inline on the suspended expression. Verified separately that the new clock/listener lifecycle does not leak: each armed timer reclaims its boundClocks entry and listener at fire or clear.

Pullfrog | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread mobile/src/transport/socket-deadline-suspension.ts
innocarpe and others added 3 commits October 4, 2026 17:46
iOS freezes setTimeout, so a 12s connect timer can fire hours later and the
diagnostics log says the endpoint is unreachable. A deadline that fires more
than a minute past its budget is logged as suspension, then the socket is
closed so the existing retry runs. A timer that fires on its budget still
reports connect-timeout or handshake-timeout.

Fixes stablyai#18829
The deadline now keeps a monotonic elapsed time and whether the app left the foreground. A jump while the app stays active remains a host timeout, and leaving the foreground is still suspension even if the wall clock jumps backward.

Co-authored-by: Cursor <cursoragent@cursor.com>
@innocarpe
innocarpe force-pushed the fix-18829-suspended-dial-timeout branch from 6e08f29 to d2c52a0 Compare October 4, 2026 08:47

@pullfrog pullfrog Bot left a comment

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.

✅ No new issues found.

Reviewed changes

  • Made the foreground signal corroborating — describeSocketDeadline now requires a late wall or monotonic clock alongside leftForeground, so a transient inactive/background blip no longer reclassifies an on-budget timer as suspended-dial. This addresses the prior [!IMPORTANT] finding and restores the PR body's guarantee that an on-budget/≤60s-late timer still reports connect-timeout / handshake-timeout.
  • Detected dials armed while already backgrounded — watchForegroundLoss now calls onLeave() at arm time when AppState.currentState is already inactive/background, so a suspension that began before the timer was armed is still captured.
  • Added regression coverage — new socket-deadline-suspension.test.ts and socket-deadline-clock.test.ts cases pin that foreground loss with on-budget clocks, a transient inactive→active flicker, and a backward wall-clock step all stay host timeouts, while both-clocks-long and already-backgrounded inputs read as suspension.

The classifier change is behavior-preserving for genuine suspensions (a frozen timer still shows a late clock, so leftForeground remains sufficient when performance.now does not advance across the freeze) and closed the earlier false positive. Clock/listener lifecycle remains clean: each armed timer reclaims its boundClocks entry and AppState listener at fire or via releaseSocketTimer.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

This branch has not been deployed

No deployments
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.

Mobile: iOS background suspension reported as "endpoint unreachable"; diagnostics shows a stale Host Orca version as current

1 participant