Skip to content

fix: keep Concierge side panel question + thinking indicator visible across clock-skew race - #94814

Merged
Gonals merged 23 commits into
Expensify:mainfrom
dilshodmackbook-sketch:dilshod/fix-90974-concierge-skew
Jul 24, 2026
Merged

fix: keep Concierge side panel question + thinking indicator visible across clock-skew race#94814
Gonals merged 23 commits into
Expensify:mainfrom
dilshodmackbook-sketch:dilshod/fix-90974-concierge-skew

Conversation

@dilshodmackbook-sketch

@dilshodmackbook-sketch dilshodmackbook-sketch commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Explanation of Change

When a user with prior Concierge history asks Concierge from search, clock skew could make the exchange disappear from the side panel in two ways:

  1. The just-sent question and "Concierge is thinking…" briefly show, then vanish and the panel collapses to "Hi there, how can I help?" — the question only reappears after clicking Show history.
  2. Concierge's reply sometimes never shows at all, even after it has arrived ("sometimes no response from Concierge").

Root cause: the Concierge "hidden history" view decides whether an action belongs to the current session by comparing created timestamps against sessionStartTime. But those timestamps come from different clocks:

  • sessionStartTime is stamped with the raw client clock (DateUtils.getDBTime()).
  • The outgoing optimistic question is stamped via getDBTimeWithSkew, which only applies skew when it is positive — so when the client runs ahead of the server (negative skew) it stays on the raw, ahead client clock.
  • Concierge's reply carries an authoritative server timestamp.

So when the client clock runs ahead of the server, sessionStartTime and the user's question sit on the ahead client clock while the reply sits on the (behind) server clock. The reply's created lands below sessionStartTime / firstUserMessageCreated, falls outside the session, and is filtered out — the question can flicker away and the reply may never appear. This poisons the four session checks: hasUserSentMessage, hasSessionActivity, firstUserMessageCreated, and isCurrentSessionAction.

Fix: make every comparison same-clock by anchoring both anchors to the server clock at the source, rather than heuristically tracking membership after the fact.

  • Add getServerAnchoredDBTime in @libs/NetworkState, which applies networkTimeSkew in both directions so the value tracks the server clock even when the client is ahead. getDBTimeWithSkew only pushes forward (to avoid reordering messages in its other lastReadTime / ordering call-sites), so it can't be reused here.
  • Stamp sessionStartTime (the side panel boundary in SidePanelContextProvider and the main-DM boundary in ConciergeSessionContext) through it.
  • Stamp the Concierge question's optimistic created through it via a new anchorCreatedToServer flag on buildOptimisticAddCommentReportAction, applied only for Concierge chats in addActions.

Now sessionStartTime, the user's question, and Concierge's reply are all on the server clock, so the existing created >= sessionStartTime / firstUserMessageCreated comparisons work unchanged. The arrival-based useCurrentSessionActionIDs heuristic is removed; the shared isCurrentUserPendingAddAction predicate is kept as a safety so a still-optimistic (pending) question is counted even before its created is comparable. getDBTimeWithSkew is left untouched, so non-Concierge behavior and message ordering are unchanged.

Fixed Issues

$ #90974
PROPOSAL: #90974 (comment)

Tests

Precondition: the test account has a previous chat history with Concierge.

  1. Open the app and navigate to any conversation or workspace chat
  2. Click the search icon in the top right corner
  3. Type a query intended for Concierge (e.g. "How to connect QBD")
  4. Click "Ask Concierge" shown below the search input results
  5. Verify the side panel shows the just-sent question with "Concierge is thinking…" underneath it
  6. Wait until the reply arrives and verify the question + reply stay visible (the panel does NOT collapse to "Hi there, how can I help?")
  7. Verify prior Concierge history remains hidden until "Show history" is clicked
  8. Click "Show history" and verify the prior history appears together with the current question and reply
  • Verify that no errors appear in the JS console

Offline tests

  1. Go offline
  2. Ask Concierge from search as in the Tests section
  3. Verify the question and "Concierge is thinking…" stay visible and the panel does not collapse to the welcome greeting
  4. Go back online and verify the message syncs and the reply arrives, with the view unchanged (no flicker, prior history still hidden until "Show history")

QA Steps

Same as Tests.

  • Verify that no errors appear in the JS console

PR Author Checklist

  • I linked the correct issue in the ### Fixed Issues section above
  • I wrote clear testing steps that cover the changes made in this PR
    • I added steps for local testing in the Tests section
    • I added steps for the expected offline behavior in the Offline steps section
    • I added steps for Staging and/or Production testing in the QA steps section
    • I added steps to cover failure scenarios (i.e. verify an input displays the correct error message if the entered data is not correct)
    • I turned off my network connection and tested it while offline to ensure it matches the expected behavior (i.e. verify the default avatar icon is displayed if app is offline)
    • I tested this PR with a High Traffic account against the staging or production API to ensure there are no regressions (e.g. long loading states that impact usability).
  • I included screenshots or videos for tests on all platforms
  • I ran the tests on all platforms & verified they passed on:
    • Android: Native
    • Android: mWeb Chrome
    • iOS: Native
    • iOS: mWeb Safari
    • MacOS: Chrome / Safari
  • I verified there are no console errors (if there's a console error not related to the PR, report it or open an issue for it to be fixed)
  • I followed proper code patterns (see Reviewing the code)
    • I verified that any callback methods that were added or modified are named for what the method does and never what callback they handle (i.e. toggleReport and not onIconClick)
    • I verified that comments were added to code that is not self explanatory
    • I verified that any new or modified comments were clear, correct English, and explained "why" the code was doing something instead of only explaining "what" the code was doing.
    • I verified any copy / text that was added to the app is grammatically correct in English. It adheres to proper capitalization guidelines (note: only the first word of header/labels should be capitalized), and is either coming verbatim from figma or has been approved by marketing (in order to get marketing approval, ask the Bug Zero team member to add the Waiting for copy label to the issue)
  • If a new code pattern is added I verified it was agreed to be used by multiple Expensify engineers
  • I followed the guidelines as stated in the Review Guidelines
  • I tested other components that can be impacted by my changes (i.e. if the PR modifies a shared library or component like Avatar, I verified the components using Avatar are working as expected)
  • If any new file was added I verified that:
    • The file has a description of what it does and/or why is needed at the top of the file if the code is not self explanatory
  • If a new CSS style is added I verified that:
    • A similar style doesn't already exist
    • The style can't be created with an existing StyleUtils function (i.e. StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))
  • If new assets were added or existing ones were modified, I verified that:
    • The assets are optimized and compressed (for SVG files, run npm run compress-svg)
    • The assets load correctly across all supported platforms.
  • If the PR modifies code that runs when editing or sending messages, I tested and verified there is no unexpected behavior for all supported markdown - URLs, single line code, code blocks, quotes, headings, bold, strikethrough, and italic.
  • If the PR modifies a generic component, I tested and verified that those changes do not break usages of that component in the rest of the App (i.e. if a shared library or component like Avatar is modified, I verified that Avatar is working as expected in all cases)
  • If the PR modifies a component related to any of the existing Storybook stories, I tested and verified all stories for that component are still working as expected.
  • If the PR modifies a component or page that can be accessed by a direct deeplink, I verified that the code functions as expected when the deeplink is used - from a logged in and logged out account.
  • If the PR modifies the UI (e.g. new buttons, new UI components, changing the padding/spacing/sizing, moving components, etc) or modifies the form input styles:
    • I verified that all the inputs inside a form are aligned with each other.
    • I added Design label and/or tagged @Expensify/design so the design team can review the changes.
  • I added unit tests for any new feature or bug fix in this PR to help automatically prevent regressions in this user flow.
  • If the main branch was merged into this PR after a review, I tested again and verified the outcome was still expected according to the Test steps.

Screenshots/Videos

Android: Native
android.mov
Android: mWeb Chrome
Screen.Recording.2026-07-21.at.01.22.23.mov
iOS: Native
ios.mov
iOS: mWeb Safari
Screen.Recording.2026-07-21.at.01.24.04.mov
MacOS: Chrome / Safari
Screen.Recording.2026-07-21.at.01.17.40.mov
Screen.Recording.2026-07-21.at.01.18.01.mov

…across clock-skew race

The Concierge hidden-history view classified the just-sent Ask Concierge
message as pre-session history when its skew-adjusted created timestamp
landed before sessionStartTime, collapsing the panel to the welcome
greeting and suppressing the thinking indicator. Treat the current user's
optimistic pending-add comment as a current-session message across all four
session-boundary checks via a shared isCurrentUserPendingAddAction predicate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dilshodmackbook-sketch
dilshodmackbook-sketch requested review from a team as code owners June 29, 2026 09:19
@melvin-bot
melvin-bot Bot requested review from JmillsExpensify and thelullabyy and removed request for a team June 29, 2026 09:19
@melvin-bot

melvin-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

@thelullabyy Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@melvin-bot
melvin-bot Bot requested review from a team and Gonals and removed request for a team June 29, 2026 09:19
@melvin-bot

melvin-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

@Gonals Please copy/paste the Reviewer Checklist from here into a new comment on this PR and complete it. If you have the K2 extension, you can simply click: [this button]

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a8879b935e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +200 to +201
function isCurrentUserPendingAddAction(reportAction: OnyxInputOrEntry<ReportAction>, currentUserAccountID?: number): boolean {
return !!currentUserAccountID && reportAction?.actorAccountID === currentUserAccountID && reportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD;

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 Keep skewed Concierge messages visible after ADD succeeds

This only treats the message as current-session while pendingAction === ADD, but addActions() clears that flag in its successData without changing the optimistic created timestamp. In the clock-skew case this patch is meant to handle, the AddComment success can arrive before Concierge replies, so the same just-sent action immediately stops matching these checks again and the side panel can collapse back to the welcome/history state until a later action arrives. Consider retaining another current-session marker or comparing against the skew-adjusted send time after pendingAction is cleared.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, this was a real gap. The pendingAction === ADD check only held during the optimistic window, so once addActions cleared the flag in its successData the skewed
message dropped back out of the session before Concierge replied.
Addressed in 59d4230: a small useCurrentSessionUserActionIDs hook records the IDs of the user's own messages captured while pending and keeps them in-session for the rest of the
session, independent of pendingAction. Added a unit test for the post-success case.

P2 Badge Keep skewed Concierge messages visible after ADD succeeds

This only treats the message as current-session while pendingAction === ADD, but addActions() clears that flag in its successData without changing the optimistic created timestamp. In the clock-skew case this patch is meant to handle, the AddComment success can arrive before Concierge replies, so the same just-sent action immediately stops matching these checks again and the side panel can collapse back to the welcome/history state until a later action arrives. Consider retaining another current-session marker or comparing against the skew-adjusted send time after pendingAction is cleared.

Useful? React with 👍 / 👎.

dilshodmackbook-sketch and others added 2 commits June 29, 2026 15:03
…s clears pendingAction

Track the current user's own session messages by ID (captured while pendingAction
=== ADD) so a clock-skewed just-sent message keeps counting as current-session even
after the AddComment success clears its pendingAction but before Concierge replies.
Previously the message fell out of the session the moment pendingAction was cleared,
collapsing the side panel back to the welcome/history state and hiding the thinking
indicator until a later action arrived.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Looks like you've decreased code coverage for some files. Please write tests to increase, or at least maintain, the existing level of code coverage. See our documentation here for how to interpret this table.

Files with missing lines Coverage Δ
.../components/SidePanel/SidePanelContextProvider.tsx 87.75% <ø> (ø)
src/hooks/useConciergeSidePanelReportActions.ts 95.12% <100.00%> (+0.31%) ⬆️
src/hooks/useReportActionsVisibility.ts 94.44% <100.00%> (+0.15%) ⬆️
src/hooks/useShouldSuppressConciergeIndicators.tsx 100.00% <100.00%> (+6.66%) ⬆️
src/libs/NetworkState.ts 94.17% <100.00%> (+0.80%) ⬆️
src/libs/ReportActionsUtils.ts 78.45% <100.00%> (+0.02%) ⬆️
src/libs/ReportUtils.ts 86.29% <100.00%> (-0.74%) ⬇️
src/pages/inbox/ConciergeSessionContext.tsx 55.55% <0.00%> (ø)
src/libs/actions/Report/index.ts 71.58% <60.00%> (+0.86%) ⬆️
... and 436 files with indirect coverage changes

dilshodmackbook-sketch and others added 2 commits June 29, 2026 15:35
…onciergeIndicators

Add tests that exercise hasSessionActivity (welcome-state suppression, a reply after
the session start, and the current user's clock-skewed optimistic message), restoring
diff coverage for the predicate the fix introduced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Build report actions via createRandomReportAction and type the mocked hooks with
jest.mocked instead of `as ReportAction` / `as jest.Mock` casts, which CI's
type-aware lint rejects.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@JmillsExpensify JmillsExpensify 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.

LGTM

Calling setState inside the effect tripped react-hooks/set-state-in-effect
(cascading renders). Move the accumulation to the render body using the
adjust-state-during-render pattern, which keeps the returned set correct within
the same render and avoids the effect entirely.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

@thelullabyy this is ready for your review whenever you get a chance — all checks are green, and the testing steps + videos are in the PR description. Let me know if you need anything else.

@thelullabyy

Copy link
Copy Markdown
Contributor

@dilshodmackbook-sketch Please add recording to all envs

@thelullabyy

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d638851ac0

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +153 to +154
isCurrentUserPendingAddAction(action, currentUserAccountID) ||
currentSessionUserActionIDs.has(action.reportActionID) ||

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 Include captured IDs in the main Concierge DM filter

When the clock-skewed optimistic message is in the main Concierge DM, hasUserSentMessage is now set from currentSessionUserActionIDs, but isCurrentSessionAction returns earlier for isConciergeMainDM using only action.created >= sessionStartTime. In an existing Concierge chat with hidden history and a just-sent ADD whose created is before sessionStartTime, the welcome state is disabled but the user's message is still filtered out until a later action arrives. Apply the pending/captured-ID check before or inside the main-DM branch as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. The main-DM branch of isCurrentSessionAction was only doing the timestamp comparison, so I ORed in the same pending/captured-ID predicate used in the side-panel branch. Now a skewed just-sent ADD stays in-session and is rendered immediately in the main DM too, consistent across both branches.

dilshodmackbook-sketch and others added 2 commits June 30, 2026 11:33
The main-DM branch of isCurrentSessionAction only checked
action.created >= sessionStartTime, so a clock-skewed just-sent ADD
(created < sessionStartTime) suppressed the welcome state via
hasUserSentMessage but was itself filtered out of the chat until a
later action arrived. OR in the pending/captured-ID predicate to match
the side-panel branch and keep the fix consistent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…changes)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@dilshodmackbook-sketch
dilshodmackbook-sketch force-pushed the dilshod/fix-90974-concierge-skew branch from 4e18220 to 4b88dec Compare June 30, 2026 07:17
@dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

Heads up on the failing Jest checks: they are not real test failures. The jobs are getting OOM-killed on the runner (exit code 143), and a different shard goes down on each run. Other unrelated PRs are hitting the same exit-143 in this same window, so it looks like a repo-wide runner issue rather than anything in this PR. Locally the related suites pass. I'll re-trigger once the runners settle.

@dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

Updated — reworked to the server-anchored approach we agreed on.

sessionStartTime and the Concierge question's created now go through getServerAnchoredDBTime (skew applied in both directions), so they land on the same clock as Concierge's server-stamped reply and the existing session checks work unchanged. getDBTimeWithSkew is left untouched since it's shared elsewhere, and useCurrentSessionActionIDs is removed entirely.

Added tests for the new helper plus a hook-level regression test that the reply stays visible under negative skew. Tested locally across the flow, no console errors — ready for another look.

@thelullabyy

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 66237c00b1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/libs/ReportUtils.ts Outdated
automatic: false,
avatar: allPersonalDetails?.[accountID]?.avatar,
created: getDBTimeWithSkew(Date.now() + createdOffset),
created: anchorCreatedToServer ? getServerAnchoredDBTime(Date.now() + createdOffset) : getDBTimeWithSkew(Date.now() + createdOffset),

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 Keep Concierge optimistic timestamps monotonic

When a client clock is ahead and networkTimeSkew becomes known or more negative between two Concierge sends (for example, an OpenReport/OpenApp response arrives after the first optimistic send), this branch applies the negative skew to the later action but not the earlier one. Report actions are sorted by created, so the second message can receive an earlier timestamp and render above the first; getDBTimeWithSkew avoided exactly this reordering. Please keep Concierge optimistic created values monotonic while still using a server-anchored boundary for filtering.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch. I kept the value server-anchored rather than reverting to getDBTimeWithSkew, because firstUserMessageCreated (the reply-visibility floor) is derived from the question's created — if it drops back to the client clock the server-stamped reply falls below it again.

Instead, getServerAnchoredDBTime now clamps forward past the report's last action (lastVisibleActionCreated), so a later send can never sort above an earlier one while the value stays server-anchored for filtering. The clamp never fires in the common stable-skew case, so created is unchanged there. Added unit tests for it.

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.

@dilshodmackbook-sketch can you check this one? Let's try to get this PR merge this week

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yep, this is handled in the latest commit (4d0695754ac). getServerAnchoredDBTime now clamps the anchored created forward past the report's last action (lastVisibleActionCreated), so a later send can never sort above an earlier one even if skew turns negative between two sends. It stays server-anchored for filtering, and the clamp is a no-op in the normal stable-skew case. Added unit tests for it — more detail in the thread just above. Should be good to go.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — switched to date-fns-tz toDate(notBeforeDBTime, {timeZone: 'UTC'}), which is how DateUtils parses DB times elsewhere, so no more manual replace(' ', 'T') + 'Z'.

Address review: getServerAnchoredDBTime applies skew in both directions, so if
networkTimeSkew turns (more) negative between two Concierge sends, a later optimistic
`created` could land before an earlier one and render above it — report actions sort
by `created`. getDBTimeWithSkew avoided this by only pushing forward.

Reverting the anchor isn't an option: firstUserMessageCreated (the reply-visibility
floor) is derived from the question's `created`, so it must stay server-anchored or the
server-stamped reply falls below it again. Instead, clamp the anchored `created` forward
past the report's last action (lastVisibleActionCreated) via a new notBeforeDBTime
argument, so a later send never sorts above an earlier one while the value stays
server-anchored for filtering. No clamp fires in the common (stable-skew) case.

Add unit tests for the clamp.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
const [reportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`);
const actionsList = Object.values(reportActions ?? {});

const hasSessionActivity =

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.

This hook feeds ComposerTypingIndicator and ConciergeThinkingMessage, both live in the Concierge chat. Now they re-render on every report-action change in that report (every incoming message, edit, reaction, read receipt), instead of only when the suppress boolean flips.

Maybe we can use the selector could have been kept and simply closed over currentUserAccountID (it's already a component-scope value) plus the pendingAction check — exactly what the other two touched hooks do inline. Basically, I think we should restore the selector form to avoid the perf regression @dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call — restored the selector so the boolean is derived inside useOnyx and the indicators only re-render when it flips, not on every report-action change. Kept the pendingAction check and closed over currentUserAccountID in the selector, matching the other two hooks.

Comment thread src/libs/NetworkState.ts Outdated
const datetime = timestamp ? new Date(timestamp) : new Date();
let anchoredMs = datetime.valueOf() + networkTimeSkew;
if (notBeforeDBTime) {
const floorMs = new Date(`${notBeforeDBTime.replace(' ', 'T')}Z`).valueOf();

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.

Can we use time/date lib to handle replace(' ', 'T') + 'Z? @dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done — using date-fns-tz toDate(notBeforeDBTime, {timeZone: 'UTC'}) now, the same helper DateUtils uses to parse DB times, so no more manual replace(' ', 'T') + 'Z'.

dilshodmackbook-sketch and others added 2 commits July 21, 2026 00:52
Address review: replace the manual `replace(' ', 'T') + 'Z'` DB-time parsing in
getServerAnchoredDBTime with date-fns-tz `toDate(..., {timeZone: 'UTC'})`, matching how
DateUtils parses DB times elsewhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address review: reading the whole report-actions object and computing the boolean in the
component body made useShouldSuppressConciergeIndicators (and its consumers
ComposerTypingIndicator / ConciergeThinkingMessage) re-render on every report-action
change. Restore the selector form so the hook only re-renders when the boolean flips; the
selector closes over the component-scope currentUserAccountID and keeps the pendingAction
check, matching what the other two hooks do inline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thelullabyy

Copy link
Copy Markdown
Contributor

@dilshodmackbook-sketch Let's retest the PR again since we have done a big refactor here

@dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

Retested after the refactor:

  • Re-ran the full Concierge + Report / ReportActionsUtils unit suites (1483 tests) — all green, including the skew-regression test (reply stays visible under negative skew) and the new monotonic-clamp test.
  • Re-verified the end-to-end flow on Chrome + Safari: the question sends and stays visible through the thinking delay, the reply arrives, the panel doesn't collapse to the welcome greeting, and prior history stays hidden until Show history. Refreshed those recordings.

It's a shared-JS change (timestamp filtering + the Onyx selector), so native behavior is identical. Happy to re-run anything specific if you'd like.

@thelullabyy

Copy link
Copy Markdown
Contributor

@codex review

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 407f7c0a6d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/libs/NetworkState.ts
Comment on lines +245 to +246
if (Number.isFinite(floorMs) && anchoredMs <= floorMs) {
anchoredMs = floorMs + 1;

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 Avoid clamping Concierge questions onto a future client clock

When notBeforeDBTime is a prior Concierge action that was stamped from the ahead client clock (for example an existing optimistic/successful message from before this fix, or after skew changes between sends), this branch moves the new server-anchored question forward to that future timestamp. The side-panel filter then uses that question as firstUserMessageCreated and only keeps replies with action.created >= firstUserMessageCreated, so a server-stamped Concierge reply that arrives in the real server interval before this artificial future time is hidden again until history is shown.

Useful? React with 👍 / 👎.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch, and it's actually sharper than a corner case. For the question, the anchored value is Date.now() + createdOffset + networkTimeSkew = the message's true server time, so the clamp fires only when lastVisibleActionCreated >= server-now — i.e. only when the prior action is itself on an ahead client clock. Whenever it fires, it pushes the question into the future, and since firstUserMessageCreated is derived from that created, a server-stamped reply lands below it and gets filtered out. So the clamp can never both fire and stay server-safe — one created can't be a monotonic sort key against a future-clocked predecessor and a reply-visibility floor at the same time.

Fixed by decoupling the two jobs instead of touching the clamp: firstUserMessageCreated only ever needs to trim the user's own pre-question messages, so I now apply that floor to current-user actions only. Concierge replies are server-stamped and already bounded by the server-anchored sessionStartTime, so they no longer depend on the question's (possibly clamped) created and stay visible. The monotonic clamp is unchanged, so the previous ordering fix still holds. Added a unit test covering a question clamped forward onto an ahead client clock with a reply arriving below it. (3fee1d78487)

…estion timestamp

The monotonic clamp on the question's `created` only ever fires when the prior
action sits on an ahead client clock, pushing the question into the future. Since
firstUserMessageCreated is derived from that `created`, a server-stamped reply lands
below it and is filtered out. Apply the firstUserMessageCreated floor to current-user
actions only; Concierge replies stay bounded by the server-anchored sessionStartTime.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@thelullabyy

Copy link
Copy Markdown
Contributor

Overall changes look good now. I will upload recording shortly...

@thelullabyy thelullabyy 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.

LGTM

@dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

LGTM

thanks a lot

@Gonals
Gonals merged commit 1894c8c into Expensify:main Jul 24, 2026
35 of 36 checks passed
@github-actions

Copy link
Copy Markdown
Contributor

🚧 Gonals has triggered a test Expensify/App build. You can view the workflow run here.

@OSBotify

Copy link
Copy Markdown
Contributor

✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to staging by https://github.com/Gonals in version: 9.4.45-0 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

@MelvinBot

Copy link
Copy Markdown
Contributor

Help site review: no changes required

I reviewed the changes in this PR and no updates to the help site (App/docs/articles) are required, so I did not create a draft docs PR.

Why: This is an internal bug fix for a clock-skew race condition in the Concierge side panel. It anchors sessionStartTime and the optimistic question created timestamp to the server clock (via a new getServerAnchoredDBTime in NetworkState) so the "hidden history" session checks stay same-clock. The change restores the expected behavior — the just-sent question, the "Concierge is thinking…" indicator, and Concierge's reply stay visible instead of flickering away.

There is no new feature, setting, tab, label, button, or user-facing workflow introduced or changed. The Concierge experience behaves the way the help site already describes it should, so nothing in App/docs/articles becomes inaccurate or incomplete as a result of this PR.

If you believe there's a specific documented behavior this change alters, let me know which article and I'll take another look.


@dilshodmackbook-sketch — since no help site changes are required, there's no linked docs PR to review or mark Ready for review. If you'd like me to document this behavior anyway, just point me at the relevant article and I'll create the draft PR.

@jponikarchuk

Copy link
Copy Markdown

This PR failing because of the issue #97187
This issue is reproducible in: Android app

@dilshodmackbook-sketch

Copy link
Copy Markdown
Contributor Author

Thanks for flagging @jponikarchuk. This one isn't introduced by this PR — it's a pre-existing gap on the Android / main-DM path (this PR fixed the side-panel version for #90974). I've traced it and left a proposal with the root cause and fix on #97187.

@OSBotify

Copy link
Copy Markdown
Contributor

🚀 Deployed to production by https://github.com/marcaaron in version: 9.4.45-14 🚀

platform result
🕸 web 🕸 success ✅
🤖 android 🤖 success ✅
🍎 iOS 🍎 success ✅

Bundle Size Analysis (Sentry):

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.

7 participants