Repository navigation
Resolve optimistic agent accountID via the replace pattern - #100068
Conversation
When CREATE_AGENT succeeds, the client's optimistic agent identity is cleared and the real data lands under a server-assigned accountID, so any open agent settings screen keyed on the optimistic ID fell through to the not-found view. Mirror replaceOptimisticReportWithActualReport: a global Onyx listener consumes the optimisticAgentAccountIDMapping entry the backend sends, redirects any focused agent settings screen to the real accountID via Navigation.setParams, and only then clears the optimistic personal detail, agent prompt, and the consumed mapping entry. createAgent no longer clears those keys in successData, so the redirect can never race the cleanup. Mirror HandleUnusedOptimisticID for the offline queue: a new middleware rewrites the ongoing and persisted requests when the mapping arrives, replacing the optimistic accountID in request data and in the stored Onyx-update arrays (successData, failureData, finallyData, optimisticData, queueFlushedData). Strings are rewritten with deepReplaceKeysAndValues; numeric agentAccountID params need a separate strict-equality pass because that util only replaces strings.
… routes The replace module now also: remaps the agent DM's participants from the optimistic accountID to the real one (a no-op when the CreateAgent response already swapped them), migrates the optimistic personal detail and agent prompt onto the real Onyx keys instead of deleting pending offline edits (dropping only the creation's ADD pendingAction), and rewrites the accountID param on every agent settings route in the navigation state rather than only the focused one. Agent update and delete actions resolve their accountID through a session-lifetime map of consumed mappings, so a write fired from a stale screen after reconciliation still targets the real account.
Classify optimisticAgentAccountIDMapping as a safe export key and update the createAgent success-data test, since cleanup of the optimistic entries now happens in replaceOptimisticAgentWithActualAgent instead of successData. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extract the shared request-rewriting logic in the middleware into a rewriteRequest helper and rewrite comments as plain sentences per the comment style guidelines. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
… onto the real agent Skip malformed or identity mapping entries before rewriting queued requests or migrating optimistic data, copy only pending/error state (and edits it staged) onto the real personal detail and prompt so server-provided base fields win, and trim comments to explain intent rather than restate the code.
…nd report hydration - Write responses only reach Onyx once the sequential queue has drained, so every request queued against the optimistic agent has already been rewritten to the real accountID by the middleware and completed by the time the mapping is observable. Copying pending markers from the optimistic keys chained after that success data and left the real agent permanently pending or resurrected a deleted one, so the listener now only redirects, repairs DM participants and clears. - Defer the redirect and cleanup until Navigation.isNavigationReady() so a persisted mapping consumed during app start cannot clear the data under an open agent screen before it is redirected. - Wait for the report collection to hydrate before consuming the mapping so the participant repair does not run against an empty collection on app start. - Leave the CreateAgent that produced the mapping untouched in the middleware and only re-persist the queued requests that actually reference the optimistic accountID.
…time A queued write's response is only flushed to Onyx after the whole sequential queue drains, so replaceOptimisticAgentWithActualAgent could observe the mapping long after the middleware swept the persisted queue. An agent edit fired in that window was enqueued with the optimistic accountID after the sweep and reached the server as an unknown account. Move the consumed-mapping map into its own AgentAccountIDMapping module (the replace module is lazy-loaded post-splash, so the middleware cannot import it directly without pulling navigation into the startup path) and register validated mapping entries there as soon as the middleware sees them, closing the window.
|
@marufsharifi 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] |
|
This PR adds a new |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
tgolen
left a comment
There was a problem hiding this comment.
I approve the connectWithoutView() references. I really like the direction of this PR. It's a much cleaner approach to correcting the optimistic IDs.
|
@marufsharifi could you please approve so we can merge? |
|
@NicolasBonet, could you please confirm whether the following behavior is expected? When an Agent is created while offline, the text containing the Agent’s temporary ID changes after reconnecting. Thank you! Screen.Recording.2026-09-08.at.8.57.52.PM.mp4 |
That is intended, people will highly likely be only doing that ID reference to refer to the agent so we need to use the latest one always. |
|
@carlosmiceli 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] |
|
🚧 NicolasBonet has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
🚀 Deployed to staging by https://github.com/NicolasBonet in version: 9.4.76-0 🚀
|
|
🤖 No help site changes are required — no docs PR created. This PR is an internal state-management fix only. It changes how the app resolves an optimistic agent What I checked
The user-visible effect is that a bug stops happening: the "Hmm... it's not here" page no longer flashes after creating an agent, and edits queued offline now apply once you're back online. Both match what Create-and-Use-Custom-Agents.md already tells users to expect, so the article is correct as written. Per Since no docs changes are needed, I didn't create a draft PR, apply the @NicolasBonet, if you think a user-facing behavior here should be documented that I've missed — for example agent creation while offline as an explicitly supported flow — reply with view run · no recording available |
|
🚀 Deployed to production by https://github.com/mountiny in version: 9.4.76-1 🚀
Bundle Size Analysis (Sentry): |
Explanation of Change
The problem
accountID. Only the server can create the realaccountID, because the agent's login is based on it.CreateAgentsuccess response deleted the temporary agent data right away. If an agent settings screen was still open on the temporaryaccountID, it showed the "Hmm... it's not here" page.accountID, which the server does not know, so they failed with a 404.The fix — the same replace pattern we already use for reports:
OPTIMISTIC_AGENT_ACCOUNT_ID_MAPPING: theCreateAgentsuccess response writes a{optimisticAccountID: realAccountID}entry there.replaceOptimisticAgentWithActualAgent(mirrorsreplaceOptimisticReportWithActualReport) listens to that key. For each entry it:accountIDon every agent settings route in the navigation state, including routes hidden behind other screens, so the open screen keeps working;accountIDto the real one (does nothing if the server response already did this);createAgent()'ssuccessData, so it can never run before the redirect.accountIDthrough it. A write started from an old screen still reaches the real account.ReplaceOptimisticAgentAccountID(mirrorsHandleUnusedOptimisticID) rewrites the requests still waiting in the offline queue: it replaces the temporaryaccountIDin the request data and in the stored Onyx updates (successData,failureData,finallyData,optimisticData,queueFlushedData), both as a string (inside Onyx keys likesharedNVP_agentPrompt_<accountID>) and as a number (theagentAccountIDparameter). Mapping entries are validated first, so a broken entry can never damage the queue.Fixed Issues
$ #93272
PROPOSAL: https://expensify.slack.com/archives/C03TQ48KC/p1788192129722359
Tests
Offline tests
QA Steps
Same as tests.
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectionAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
MacOS: Chrome / Safari
CleanShot.2026-09-07.at.2.02.48.PM.mp4