You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Agents - Blank rows appear in agents table after deleting agents (stale local Onyx entries) #96889
Blank rows appear in the Settings > Agents table after deleting agents. Reported by shawnborton on #95991 (comment) — blank rows showed on a branch build but not on staging, after previously deleting a bunch of agents.
Root cause
The agents table renders one row per sharedNVP_agentPrompt_<accountID> collection key whose matching personalDetailsList[accountID] still exists (AgentsPage.tsx:82-107). A row is only skipped when personal details are missing entirely; if they exist but have an empty displayName/login, the row renders blank.
deleteAgent clears both the prompt key and the personal-details entry on the DELETE_AGENT success response (Agent.ts:289-309), but OPEN_AGENTS_PAGE only merges in what the server returns and never prunes local keys (Agent.ts:23-32). So any orphaned sharedNVP_agentPrompt_* entries left behind by earlier deletes (e.g. deleted offline, or from a build predating current cleanup logic) linger in local Onyx and render as blank rows. Staging looked clean because it had a different/fresher dataset.
Expected behavior
Deleted agents should never leave orphaned sharedNVP_agentPrompt_* entries behind, and the agents table should not render a row for an agent that has no valid personal details (empty name/login).
Reproduction
Create several agents in Settings > Agents.
Delete them (ideally reproduce with a delete that doesn't cleanly land the success response, e.g. offline then reconnect).
Reopen Settings > Agents.
Observe blank rows in the table.
Possible fixes to explore
Have OPEN_AGENTS_PAGE return the full agent set and reset the sharedNVP_agentPrompt collection so stale local keys are pruned, or
Guard the row builder in AgentsPage.tsx to skip rows whose personal details have no displayName and no login.
@MelvinBot let's explore the idea of making OPEN_AGENTS_PAGE reset the proper content, let's create a PR for that and let me know about any important consideration we might need to have.
Makes openAgentsPage() reconcile local Onyx to the server's set instead of only merging on top of it. Before firing OPEN_AGENTS_PAGE it clears the existing non-pendingsharedNVP_agentPrompt_* keys optimistically, so when the response merges the current agents back in, any key the server doesn't return is pruned — killing the orphaned blank rows. Added unit tests in AgentActionTest.ts covering the clear, the pending-skip, and the offline no-op. Typecheck, lint, and all Agent unit/UI tests pass.
Important considerations
This only works if OPEN_AGENTS_PAGE returns the full current agent set. The whole approach is clear-then-repopulate, so if the backend ever returns a partial/delta response, we'd wipe real agents that just weren't re-sent. This is the one thing to confirm before merging — I can't verify the backend command's response shape from the App repo. If it's not guaranteed to be a full snapshot, we should not ship this frontend approach.
The cleanest fix is actually backend-side and lives outside App. Rather than the client guessing which local keys are stale, OPEN_AGENTS_PAGE could return the collection with replace semantics (a SET_COLLECTION, or explicit nulls for deleted agents). That prunes authoritatively, needs no optimistic clear, and avoids consideration Add Expensify eslint #3 entirely — but it's an Auth/Web-Expensify change, so it can't be done in this repo.
Brief reload flicker. Clearing optimistically and repopulating from the response means the table momentarily empties and refills on every page open (one network round-trip). Worth a quick look on a slow connection to decide if it's acceptable or needs a loading guard.
Offline is intentionally a no-op. While offline the read can't repopulate, so we skip the reset to avoid emptying the list until reconnect. Trade-off: orphaned rows persist until the next online open — acceptable, since the whole point is they're already stale.
In-flight ops are preserved. Keys with a pendingAction (optimistic add/update/delete not yet confirmed) are skipped, so we never drop local state the server hasn't acknowledged.
Complementary defensive guard (from the issue's other option). Even with this, guarding the row builder in AgentsPage.tsx to skip rows with no displayNameand no login is a cheap belt-and-suspenders that protects against any future source of orphaned/empty entries. Not included here to keep the PR focused, but easy to add if you'd like both.
My recommendation: if the backend can own the reset (#2), prefer that. Otherwise this frontend approach is viable provided #1 holds, and I'd pair it with the row guard in #6.
OpenAgentsPage returns the full authoritative agent set but was sending it with mergecollection, which never removes keys the client already has — that's the whole bug. The Auth PR switches it to setcollection so the client replaces the entire sharedNVP_agentPrompt_ collection on every Agents page open, pruning orphaned keys. Existing stale state on users' devices self-heals on the next page open; no App change or migration needed. Closed the frontend draft PR #96895 accordingly.
Activity
NicolasBonet commentedon Jul 23, 2026
@MelvinBot let's explore the idea of making OPEN_AGENTS_PAGE reset the proper content, let's create a PR for that and let me know about any important consideration we might need to have.
MelvinBot commentedon Jul 23, 2026
Draft PR up: #96895
What it does
Makes
openAgentsPage()reconcile local Onyx to the server's set instead of only merging on top of it. Before firingOPEN_AGENTS_PAGEit clears the existing non-pendingsharedNVP_agentPrompt_*keys optimistically, so when the response merges the current agents back in, any key the server doesn't return is pruned — killing the orphaned blank rows. Added unit tests inAgentActionTest.tscovering the clear, the pending-skip, and the offline no-op. Typecheck, lint, and all Agent unit/UI tests pass.Important considerations
This only works if
OPEN_AGENTS_PAGEreturns the full current agent set. The whole approach is clear-then-repopulate, so if the backend ever returns a partial/delta response, we'd wipe real agents that just weren't re-sent. This is the one thing to confirm before merging — I can't verify the backend command's response shape from the App repo. If it's not guaranteed to be a full snapshot, we should not ship this frontend approach.The cleanest fix is actually backend-side and lives outside App. Rather than the client guessing which local keys are stale,
OPEN_AGENTS_PAGEcould return the collection with replace semantics (aSET_COLLECTION, or explicitnulls for deleted agents). That prunes authoritatively, needs no optimistic clear, and avoids consideration Add Expensify eslint #3 entirely — but it's an Auth/Web-Expensify change, so it can't be done in this repo.Brief reload flicker. Clearing optimistically and repopulating from the response means the table momentarily empties and refills on every page open (one network round-trip). Worth a quick look on a slow connection to decide if it's acceptable or needs a loading guard.
Offline is intentionally a no-op. While offline the read can't repopulate, so we skip the reset to avoid emptying the list until reconnect. Trade-off: orphaned rows persist until the next online open — acceptable, since the whole point is they're already stale.
In-flight ops are preserved. Keys with a
pendingAction(optimistic add/update/delete not yet confirmed) are skipped, so we never drop local state the server hasn't acknowledged.Complementary defensive guard (from the issue's other option). Even with this, guarding the row builder in
AgentsPage.tsxto skip rows with nodisplayNameand nologinis a cheap belt-and-suspenders that protects against any future source of orphaned/empty entries. Not included here to keep the PR focused, but easy to add if you'd like both.My recommendation: if the backend can own the reset (#2), prefer that. Otherwise this frontend approach is viable provided #1 holds, and I'd pair it with the row guard in #6.
NicolasBonet commentedon Jul 23, 2026
Went with the backend fix instead of the frontend optimistic clear: https://github.com/Expensify/Auth/pull/23170
OpenAgentsPagereturns the full authoritative agent set but was sending it withmergecollection, which never removes keys the client already has — that's the whole bug. The Auth PR switches it tosetcollectionso the client replaces the entiresharedNVP_agentPrompt_collection on every Agents page open, pruning orphaned keys. Existing stale state on users' devices self-heals on the next page open; no App change or migration needed. Closed the frontend draft PR #96895 accordingly.