Skip to content

fix: WebRTC auto-reconnect on tab resume + Go Available reconnects first#341

Merged
coygg merged 1 commit into
masterfrom
fix/webrtc-tab-recovery
Apr 4, 2026
Merged

fix: WebRTC auto-reconnect on tab resume + Go Available reconnects first#341
coygg merged 1 commit into
masterfrom
fix/webrtc-tab-recovery

Conversation

@coygg
Copy link
Copy Markdown
Collaborator

@coygg coygg commented Apr 4, 2026

Tab going to background drops WebRTC. Now auto-reconnects on visibility change, Go Available reconnects before failing, cooldown bypassed for explicit recovery.

Summary by CodeRabbit

  • Bug Fixes
    • Improved connection recovery when attempting to set available status while unregistered; triggers automatic reconnection with enhanced error guidance.
    • Added automatic reconnection attempts when browser tab becomes active with a disconnected or errored connection.
    • Enhanced error messages to direct users to diagnostic tools when recovery fails.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
policyjar Ready Ready Preview, Comment Apr 4, 2026 4:37am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 4, 2026

📝 Walkthrough

Walkthrough

Enhanced connection resilience in the Telnyx context layer by adding automatic reconnection attempts when transitioning status to available without registration, implementing visibility-based reconnect triggering, and updating cooldown bypass logic to accommodate specific reconnect reasons.

Changes

Cohort / File(s) Summary
Status Change with Registration Recovery
src/components/layout/header.tsx
Added isRegisteredRef to track registration state, implemented 12-second polling (300ms intervals) to wait for registration when attempting to set status to available while unregistered, and triggers reconnect() on recovery failure with specialized error messaging directing users to Agent Diagnostic.
Visibility & Reconnect Gating
src/hooks/use-telnyx.ts
Updated triggerManualReconnect() to bypass MANUAL_RECONNECT_COOLDOWN_MS when reason is 'manual-button', 'visibility-resume', or 'go-available'; enhanced handleVisibilityChange() to immediately reconnect on tab visibility return if connection status is 'disconnected' or 'error', with 500ms recovery delay instead of 1500ms.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Header
    participant TelnyxCtx
    participant Recovery

    rect rgba(100, 150, 200, 0.5)
    Note over Header,Recovery: Flow 1: Status Change to Available (Unregistered)
    User->>Header: Click set status to available
    Header->>Header: Check isRegisteredRef
    alt isRegistered = false
        Header->>TelnyxCtx: Call reconnect()
        Header->>Header: Start polling (300ms intervals, max 12s)
        loop Poll until registered or timeout
            Header->>Header: Check isRegisteredRef
            alt Registration successful
                Header->>User: Status set to available
            end
        end
        alt Timeout
            Header->>User: Show error: "Use Agent Diagnostic → Reconnect"
        end
    else isRegistered = true
        Header->>User: Status set to available
    end
    end

    rect rgba(150, 200, 100, 0.5)
    Note over Header,Recovery: Flow 2: Tab Visibility Change
    User->>Document: Tab becomes visible
    Header->>Header: Listen to visibility change
    alt connectionStatus = 'disconnected' or 'error'
        Header->>TelnyxCtx: Call reconnect() (bypass cooldown)
        Header->>Recovery: Schedule recovery after 500ms
    else connectionStatus = 'connected'
        Header->>Recovery: Schedule recovery after 1500ms (legacy behavior)
    end
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 When tabs go dark and links do fade,
A gentle reconnect's been made.
With polling whispers, status calls,
Our bunny code now catches all! 🔗✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the two main changes: WebRTC auto-reconnect on tab resume and Go Available triggering reconnection first, directly matching the changeset objectives and implementation details.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 fix/webrtc-tab-recovery

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f3a94da477

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

isRegisteredRef.current = isRegistered
}, [isRegistered])

const waitForRegistration = useCallback(async (timeoutMs = 12000) => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Increase Go Available wait window to cover reconnect backoff

waitForRegistration caps the Go Available recovery wait at 12 seconds, but reconnect attempts in triggerManualReconnect use exponential delays (3s/6s/12s… up to 30s) before init() even runs, and init() itself can take up to the registration timeout. In those common failure-recovery paths, this function returns false before the reconnect attempt can realistically complete, so agents get a hard “cannot go available” error even though the phone may recover shortly after.

Useful? React with 👍 / 👎.

showPresenceToast('Phone reconnecting…', 'warning')
reconnect()

const recovered = await waitForRegistration()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard stale Go Available requests from overwriting newer status

This handler now awaits registration before applying the status change, but there is no cancellation/version guard for in-flight requests. If an agent clicks “Go Available” while disconnected and then chooses another status before registration completes, the delayed first call continues and can overwrite the later selection by posting available after the second change has already succeeded.

Useful? React with 👍 / 👎.

@coygg coygg merged commit 7298852 into master Apr 4, 2026
4 of 5 checks passed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/components/layout/header.tsx`:
- Around line 93-103: handleStatusChange triggers reconnect() when moving to
'available', but the reconnect implementation always uses the literal
'manual-button' reason so the 'go-available' bypass branch is never reached;
update the reconnect signature in the UseTelnyxReturn type to accept an optional
reason (e.g., reconnect: (reason?: string) => void), change the hook's reconnect
callback (where manualReconnectFnRef is invoked) to accept a reason parameter
with a 'manual-button' default, and call reconnect('go-available') from
handleStatusChange so the 'go-available' reason is actually passed through and
honored by the bypass logic.
- Around line 160-167: The useEffect in header.tsx that watches connectionStatus
and calls reconnect() duplicates the hook's visibility-based reconnect path;
remove this entire effect (the useEffect that checks document.visibilityState,
connectionStatus and schedules reconnect with setTimeout) and rely on the hook's
handleVisibilityChange / triggerManualReconnect and its runLifecycleRecovery
timing instead (symbols: the useEffect block in header.tsx, connectionStatus,
reconnect; hook symbols: handleVisibilityChange, triggerManualReconnect,
runLifecycleRecovery in use-telnyx). Ensure no other code depends on that
setTimeout retry before removing.

In `@src/hooks/use-telnyx.ts`:
- Around line 885-892: When visibility resumes and connectionStatusRef.current
is 'disconnected' or 'error', avoid invoking both
triggerManualReconnect('visibility-resume') and
runLifecycleRecovery('visibilitychange', 500) to prevent a possible
double-reconnect; change the branch in the visibility handler so it only calls
triggerManualReconnect('visibility-resume') and returns (remove the
runLifecycleRecovery call), leaving runLifecycleRecovery('visibilitychange',
1500) for the non-error path — this keeps triggerManualReconnect as the sole
immediate reconnection path and prevents ensureRegistered from being driven by
the short 500ms recovery timer.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d991e2c5-f6bf-4076-8d0d-f875c2ea10a2

📥 Commits

Reviewing files that changed from the base of the PR and between 9a20cb0 and f3a94da.

📒 Files selected for processing (2)
  • src/components/layout/header.tsx
  • src/hooks/use-telnyx.ts

Comment on lines 93 to 103
const handleStatusChange = useCallback(async (newStatus: AgentStatus): Promise<boolean> => {
if (newStatus === 'available' && !isRegistered) {
showPresenceToast('Cannot go available — phone not connected. Please refresh.', 'error')
return false
showPresenceToast('Phone reconnecting…', 'warning')
reconnect()

const recovered = await waitForRegistration()
if (!recovered) {
showPresenceToast('Cannot go available — phone not connected. Click Agent Diagnostic → Reconnect.', 'error')
return false
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Minor: 'go-available' reason is never used.

The reconnect() function (defined at lines 1200-1209 in the hook) always passes 'manual-button' as the reason. While this still bypasses cooldown correctly, the 'go-available' reason added to the bypass list at line 670 is never actually triggered from this "Go Available" flow, making that part of the bypass check dead code.

If distinguishing "Go Available" reconnects from manual button clicks in logs is valuable, consider exposing the reason parameter:

// In UseTelnyxReturn interface
reconnect: (reason?: string) => void

// In the hook's reconnect callback
const reconnect = useCallback((reason = 'manual-button') => {
  manualReconnectFnRef.current?.(reason)
}, [])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/header.tsx` around lines 93 - 103, handleStatusChange
triggers reconnect() when moving to 'available', but the reconnect
implementation always uses the literal 'manual-button' reason so the
'go-available' bypass branch is never reached; update the reconnect signature in
the UseTelnyxReturn type to accept an optional reason (e.g., reconnect:
(reason?: string) => void), change the hook's reconnect callback (where
manualReconnectFnRef is invoked) to accept a reason parameter with a
'manual-button' default, and call reconnect('go-available') from
handleStatusChange so the 'go-available' reason is actually passed through and
honored by the bypass logic.

Comment on lines +160 to +167
useEffect(() => {
if (document.visibilityState !== 'visible') return
if (connectionStatus !== 'disconnected' && connectionStatus !== 'error') return

reconnect()
const retryTimer = setTimeout(() => reconnect(), 2500)
return () => clearTimeout(retryTimer)
}, [connectionStatus, reconnect])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential duplicate reconnect triggers with hook's visibility handler.

This effect runs when connectionStatus changes to 'disconnected'/'error' while the tab is visible. However, the hook (use-telnyx.ts lines 882-894) also has a handleVisibilityChange listener that triggers triggerManualReconnect('visibility-resume') when the tab becomes visible with those same statuses.

When the tab becomes visible after backgrounding:

  1. Hook's handleVisibilityChange fires → calls triggerManualReconnect('visibility-resume')
  2. This effect re-renders (if connectionStatus is stale as 'disconnected') → calls reconnect()

Both reconnect paths will fire nearly simultaneously. While internal guards prevent duplicate init() calls, this creates redundant code paths and confusing logs.

Additionally, the 2500ms retry at line 165 may interfere with the hook's own recovery timing (500ms runLifecycleRecovery).

Consider either:

  1. Removing this effect and relying solely on the hook's visibility handler
  2. Or removing the hook's visibility-based reconnect and handling it entirely in the header
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/layout/header.tsx` around lines 160 - 167, The useEffect in
header.tsx that watches connectionStatus and calls reconnect() duplicates the
hook's visibility-based reconnect path; remove this entire effect (the useEffect
that checks document.visibilityState, connectionStatus and schedules reconnect
with setTimeout) and rely on the hook's handleVisibilityChange /
triggerManualReconnect and its runLifecycleRecovery timing instead (symbols: the
useEffect block in header.tsx, connectionStatus, reconnect; hook symbols:
handleVisibilityChange, triggerManualReconnect, runLifecycleRecovery in
use-telnyx). Ensure no other code depends on that setTimeout retry before
removing.

Comment thread src/hooks/use-telnyx.ts
Comment on lines +885 to 892

if (connectionStatusRef.current === 'disconnected' || connectionStatusRef.current === 'error') {
void triggerManualReconnect('visibility-resume')
runLifecycleRecovery('visibilitychange', 500)
return
}

runLifecycleRecovery('visibilitychange', 1500)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider potential double-reconnect on visibility resume.

When the tab becomes visible with disconnected/error status, both triggerManualReconnect('visibility-resume') and runLifecycleRecovery('visibilitychange', 500) are invoked. If triggerManualReconnect completes within 500ms (resetting isReconnectingRef), ensureRegistered may trigger another reconnect attempt.

This is likely benign given the existing guards (manualReconnectInFlightRef, cooldown checks), but the dual-path could be simplified:

  1. Rely solely on triggerManualReconnect for the immediate case (it handles full reconnection)
  2. Skip runLifecycleRecovery when triggerManualReconnect is invoked

If the intent is to have ensureRegistered verify post-reconnect state, the 500ms delay may be too short for the backoff delay in triggerManualReconnect (which starts at 3s).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/use-telnyx.ts` around lines 885 - 892, When visibility resumes and
connectionStatusRef.current is 'disconnected' or 'error', avoid invoking both
triggerManualReconnect('visibility-resume') and
runLifecycleRecovery('visibilitychange', 500) to prevent a possible
double-reconnect; change the branch in the visibility handler so it only calls
triggerManualReconnect('visibility-resume') and returns (remove the
runLifecycleRecovery call), leaving runLifecycleRecovery('visibilitychange',
1500) for the non-error path — this keeps triggerManualReconnect as the sole
immediate reconnection path and prevents ensureRegistered from being driven by
the short 500ms recovery timer.

@coygg coygg deleted the fix/webrtc-tab-recovery branch June 1, 2026 07:05
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