Skip to content

[Due for payment 2026-10-15] [$175] LHN sometimes doesn't show unread chats until the chat is opened #102973

Description

@melvin-bot

Problem

Over the past few days or weeks, chats with new messages sometimes don't appear as unread in the LHN. The unread state shows only after the user opens the chat manually.

Details

  • Platform: Unconfirmed (possibly web)
  • Reproduction steps: Not known yet. The issue happens on and off.

Next steps

  • Find reliable steps to reproduce the issue.
  • Check whether the existing LHN logging captures these cases. If it does not, add telemetry to detect when a report has unread actions but the LHN doesn't show it as unread.

Reported in Slack.

Upwork Automation - Do Not Edit
Issue OwnerCurrent Issue Owner: @dmkt9

Activity

  1. added
    BugSomething is broken. Auto assigns a BugZero manager.
    on Oct 5, 2026
  2. MelvinBot commented on Oct 5, 2026

    @MelvinBot
    Contributor

    Proposal

    Confidence: medium. The code and the PR that introduced the change support this. I couldn't check production logs in this run, so it isn't confirmed for a specific user yet.

    What is the root cause of that problem?

    The LHN is fine. On web, the new-message update never reaches the client.

    Since [No QA] fix: skip reconnectApp on pusher socket blips (merged 2026-08-29), web skips ReconnectApp when the Pusher user channel resubscribes, unless pusher-js reached the unavailable state (PusherUtils.ts:58-67, Pusher/index.ts:115-118).

    1. A socket dies silently, for example after sleep, a Wi-Fi change, or a throttled tab. pusher-js only notices this after its activity and pong timeouts.
    2. It then reconnects in under 10 seconds, so it never reaches unavailable. The resubscribe skips the sync.
    3. Pusher doesn't replay events. Messages sent while the socket was dead are lost.
    4. Gap detection only runs when the next server payload with previousUpdateID arrives (applyOnyxUpdatesReliably.ts:50-51). Opening the chat sends OpenReport, which triggers it. That's why the unread state appears only after the user opens the chat.

    PR #99084 calls this out in its own test step 10: "Verify the message does not appear yet. This is the accepted tradeoff… an idle tab stays stale until one arrives."

    What changes do you think we should make in order to solve the problem?

    1. In src/libs/Pusher/index.ts, also mark an outage when the socket drops after an activity or pong timeout (a silent death). Optionally, also mark one when the time since the last inbound socket message is longer than a few seconds. Short blips still skip ReconnectApp, and silent deaths sync again.
    2. Add a unit test to tests/unit/PusherSubscribeWebTest.ts. It should cover a pong-timeout drop followed by a fast reconnect, and check that ReconnectApp fires.
    3. For the telemetry the issue asks for, add a Log.info when gap detection runs. It should include the gap size and the time since the last "Skipping reconnect" line. Then we can measure how often skipped syncs leave clients stale.

    What alternative solutions did you explore? (Optional)

    • Revert [No QA] fix: skip reconnectApp on pusher socket blips #99084. This is simple but brings back about 54k extra ReconnectApp calls every 2 hours, per the PR.
    • An LHN rendering bug. I traced the code from isUnread to the LHN row and found no memo or cache that skips a changed report. Opening the chat repairs the data itself, which points to missing data, not a render bug.
    Investigation details

    Possibly related issues:


    Next Steps for Contributor+ team:
    To accept: @MelvinBot implement [this](https://github.com/Expensify/App/issues/102973) to create a draft PR.
    To refine: @MelvinBot <your feedback>
    To reject: Explain why you are rejecting Melvin's proposal.


    view run

  3. added
    ExternalAdded to denote the issue can be worked on by a contributor
    on Oct 5, 2026
  4. melvin-bot commented on Oct 5, 2026

    @melvin-bot
    Author

    Triggered auto assignment to Contributor-plus team member for initial proposal review - @dmkt9 (External)

  5. changed the title [-]LHN sometimes doesn't show unread chats until the chat is opened[/-] [+][$175] LHN sometimes doesn't show unread chats until the chat is opened[/+] on Oct 5, 2026
  6. melvin-bot commented on Oct 5, 2026

    @melvin-bot
    Author
  7. melvin-bot commented on Oct 5, 2026

    @melvin-bot
    Author

    📣 @dmkt9 We're missing your Upwork ID to automatically send you an offer for the Reviewer role.
    Once you apply to the Upwork job, your Upwork ID will be stored and you will be automatically hired for future jobs!

  8. camesenin commented on Oct 5, 2026

    @camesenin
    Proposal

    Please re-state the problem that we are trying to solve in this issue.

    On Web, unread chat messages do not highlight as unread in the Left Hand Navigation (LHN) until the user clicks and opens the chat report.

    What is the root cause of that problem?

    In src/libs/PusherUtils.ts (lines 60–67), when Pusher re-subscribes to the private user channel, it calls Pusher.claimOutageSync():

    unregisterPrivateUserChannelResubscribe = Pusher.onChannelResubscribe(getUserChannelName(accountID), () => {
        if (!Pusher.claimOutageSync()) {
            Log.info('[PusherUtils] Skipping reconnect, socket recovered without going unavailable');
            return;
        }
        Log.info('[PusherUtils] Pusher re-subscribed to private user channel, triggering reconnect');
        reconnect();
    });

    In src/libs/Pusher/index.ts (line 116), hasUnclaimedOutage is only set if the socket reaches the unavailable state:

    hasUnclaimedOutage ||= states.current === 'unavailable';

    When a socket connection drops silently (e.g. laptop sleep, Wi-Fi network switch, browser throttling idle tabs, or activity/pong timeout) and reconnects in under 10 seconds, pusher-js never transitions into unavailable.

    Consequently:

    1. claimOutageSync() returns false, skipping reconnect() (ReconnectApp).
    2. Pusher does not replay missed real-time events that occurred while the socket was disconnected.
    3. The report's Onyx state is never updated with the new message until the user navigates into the chat, which triggers OpenReport and fetches the latest report data.

    What changes do you think we should make in order to solve the problem?

    1. In src/libs/Pusher/index.ts:
      • Track when a socket disconnect occurs due to transport error or connection timeout (such as activity/pong watchdog drop) by setting hasUnclaimedOutage = true.
      • Ensure that any resubscription that occurs after an unnoticed connection break properly triggers ReconnectApp via claimOutageSync(), while keeping immediate blips without missed events optimized.
    2. In src/libs/actions/applyOnyxUpdatesReliably.ts:
      • Add telemetry logging in gap detection to monitor gap size and delta time since the last skipped reconnect to ensure zero dropped syncs.
    3. Unit test:
      • Add a test case in tests/unit/PusherSubscribeWebTest.ts asserting that an activity/pong timeout drop followed by a fast reconnect correctly marks an outage and triggers ReconnectApp.

    What alternative solutions did you explore? (Optional)

    • Reverting PR [No QA] fix: skip reconnectApp on pusher socket blips #99084: This would trigger excessive ReconnectApp calls on transient socket blips (~54k extra calls). Targeting actual timeout/drop disconnects rather than relying solely on unavailable preserves efficiency while eliminating stale state.
    • Client-side polling in LHN: Unnecessary and contradicts Onyx push-driven architecture.

    Contributor details
    Your Expensify account email: camesenin@gmail.com
    Upwork Profile Link: https://www.upwork.com/freelancers/~0170d6b02e9440e604

  9. 5 remaining items

  10. added
    ReviewingHas a PR in review
    and removed on Oct 6, 2026
  11. VickyStash commented on Oct 6, 2026

    @VickyStash
    Contributor

    Updates:

    • Today I was able to reproduce one of the scenarios mentioned in slack when unread message isn't display (when new message arrives in the thread):
    thread-case.mp4
  12. mountiny commented on Oct 7, 2026

    @mountiny
    Contributor

    PR merged.

  13. added and removed
    ReviewingHas a PR in review
    on Oct 8, 2026
  14. changed the title [-][$175] LHN sometimes doesn't show unread chats until the chat is opened[/-] [+][Due for payment 2026-10-15] [$175] LHN sometimes doesn't show unread chats until the chat is opened[/+] on Oct 8, 2026
  15. melvin-bot commented on Oct 8, 2026

    @melvin-bot
    Author

    @dmkt9

    The solution for this issue has been 🚀 deployed to production 🚀 in version 9.5.5-2 and is now subject to a 7-day regression period 📆. Here is the list of pull requests that resolve this issue:

    If no regressions arise, payment will be issued on 2026-10-15. 🎊

    The following checklist (instructions) will need to be completed before the issue can be closed. Please copy/paste the Contributor+ Checklist from here into a new comment on this GH and complete it. If you have the K2 extension, you can simply click: [this button]. If no checklist is needed for this issue, you can click: [no checklist button]

  16. dmkt9 commented on Oct 9, 2026

    @dmkt9
    Contributor

    Contributor+ Checklist:

    • [Contributor] The offending PR and associated issue have been commented on, pointing out the bug it caused and why, so the author and reviewers can learn from the mistake.

      Link to the comment on the PR: N/A

    • [Contributor] If the regression was CRITICAL (e.g. interrupts a core flow) A discussion in #expensify-open-source has been started about whether any other steps should be taken (e.g. updating the PR review checklist) in order to catch this type of bug sooner.

      Link to discussion: N/A

    • [Contributor] If it was decided to create a regression test for the bug, please propose the regression test steps using the template below to ensure the same bug will not reach production again.

    Regression Test Proposal

    Precondition:

    • User A has focus mode and DM with user B.
    • Open the app as User A and User B from different devices (or from incognito and not incognito windows)

    Test:

    1. As User A sends the message 'Test' to User B in DM, and switches to another chat
    2. As User B, reply in a thread to the message 'Test'
    3. From User A, see the reply arrives and is displayed as unread in the Inbox (All/Unreads tabs)

    Do we agree 👍 or 👎

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugSomething is broken. Auto assigns a BugZero manager.ExternalAdded to denote the issue can be worked on by a contributorNeeds ReproductionReproducible steps neededWeeklyKSv2

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions