Skip to content

Agents Manager: poll for the reply after a page change mid-turn - #113926

Open
AnnaMag wants to merge 6 commits into
trunkfrom
add/poll-reply-after-page-change
Open

Agents Manager: poll for the reply after a page change mid-turn#113926
AnnaMag wants to merge 6 commits into
trunkfrom
add/poll-reply-after-page-change

Conversation

@AnnaMag

@AnnaMag AnnaMag commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Part of WOOAI-872, WOOAI-847

Proposed Changes

  • useConversation: new refetchWhileAwaitingReply option. Refetch every 3s (up to 5 minutes — tool-heavy Woo turns run long) while a question is still unanswered on the server — the loaded transcript ends on a user turn, or a turn this tab sent (still pending in agenttic's local store) has no reply yet. The second signal matters: the server persists a turn only once it starts processing it, so a transcript fetched right after a page change can still end on the previous agent turn. Exposes isAwaitingReply. refetchIntervalInBackground: true so a merchant who switches tabs while waiting still gets the reply.
  • @automattic/agenttic-client: export loadConversation and messageTextContent (used to read the pending turns).
  • OrchestratorChat: enable it for non-Reader agents unless the merchant has already sent a message in this tab or a send is in progress (so a refetch can never replace a live stream); show a "Waiting for the reply…" indicator meanwhile.

Why are these changes being made?

When a merchant sends a message and the page changes before the reply comes back (own navigation, or the agent's wp_admin__navigate), the next page fetches the conversation once on mount. If the server is still answering, that fetch returns the question with no reply — and nothing ever fetches again, so the panel shows an unanswered bubble until the next full page load. Merchants read this as "the assistant didn't answer".

The turn did reach the server in this case, so no client-side recovery is needed — just keep fetching until the reply is there.

Testing Instructions

yarn jest -c test/packages/jest.config.js packages/agents-manager/src/hooks/__tests__/use-conversation.test.ts

Sandbox (Simple/Atomic): sandbox widgets.wp.com, cd apps/agents-manager && yarn dev --sync, enable Unified Chat under Automattician Options on /wp-admin/profile.php.

  1. In wp-admin, open the chat and send any message so a session exists.
  2. Send a question that takes a while to answer, then immediately change admin page.
  3. The question appears with "Waiting for the reply…"; the transcript is refetched every ~3s (Network tab: odie/chat/...) and the reply appears without reloading, indicator gone.
  4. Send a message on the new page while waiting — polling stops and the normal flow takes over.

Calypso (yarn start): same steps within the Calypso chat.

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)? — Simple, via sandboxed widgets.wp.com
  • Have you checked for TypeScript, React or other console errors?
  • For UI changes, have you tested the affected components in dark mode?
  • Have you tested accessibility for your changes? Ensure the feature remains usable with various user agents (e.g., browsers), interfaces (e.g., keyboard navigation), and assistive technologies (e.g., screen readers) (PCYsg-S3g-p2).
  • Have you used memoizing on expensive computations? More info in Memoizing with create-selector and Using memoizing selectors and Our Approach to Data
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    • For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages.
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

Navigating while the server is still answering leaves the panel with an
unanswered user bubble: the transcript is fetched once on mount and nothing
picks up the reply that lands afterwards. Refetch every 3s (up to 90s) while
the last loaded message is a user turn and the merchant has not taken over
the conversation in this tab, with a "Waiting for the reply…" indicator.

(cherry picked from commit 7954118)
AnnaMag added a commit that referenced this pull request Aug 29, 2026
Turns that already belong to a server session are reloaded by
useConversation on mount, and the reply is now polled for separately
(#113926). Keep only the local-* case: the first send of a session, orphaned
before the server assigned an id, is marked failed and offered a retry.
@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown

Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

Async-loaded Components (~359 bytes added 📈 [gzipped])

Details
Name Parsed Gzip
async-load-automattic-agents-manager +1.18 kB +0.1% +359 B +0.1%

React components that are loaded lazily, when a certain part of UI is displayed for the first time.

Legend

What is parsed and gzip size?

Parsed Size: Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
Gzip Size: Compressed size of the JS and CSS files. This much data needs to be downloaded over network.

…script

The server persists a user turn only once it starts processing it, so a
transcript fetched right after a page change can end on the previous agent
turn and hide that a question is in flight. Read the in-flight turns from the
local conversation store and keep polling until the server shows a reply to
each of them.
Always read the tab's pending turns once per session (the flag is applied
when deciding to poll), drop the unreachable catch and unmount guard, and
the redundant Reader Chat check.
@AnnaMag AnnaMag self-assigned this Aug 29, 2026
@AnnaMag
AnnaMag marked this pull request as ready for review August 29, 2026 13:24
@AnnaMag
AnnaMag requested a review from a team August 29, 2026 13:39
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Aug 29, 2026
Tool-heavy Woo turns (catalog review, batch edits) routinely exceed 90s.
loadConversation ran on every session, and a thrown read left the lag
case (transcript still ending on the previous agent turn) with nothing
to poll. Only read the local store when polling, and catch so a failed
read still follows the server transcript.
@AnnaMag
AnnaMag requested a review from wellyshen August 29, 2026 18:09

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

Great catch on the lost-reply case — definitely worth fixing! My concern is reliability: polling the hydration query replaces the whole conversation on every check, and the stop conditions can fail to terminate. Two repros:

  • The composer is fully locked while waiting — Stop aborts nothing, Enter is blocked:
Image
  • An infinite re-fetch with the PR's own scenario (send a question → change the page very quickly): the send never reached the server, so the message disappears and the refetch runs every 3s until the 5-minute timeout — there's no give-up branch, so the question is simply lost (#113591's recovery doesn't cover this case either).
2026-08-31.7.17.22.mov

Could we rework it toward a more reliable approach? Claude's suggestion, using existing primitives:

  1. Snapshot the local pending turns once at mount.
  2. Poll a cheap page-1 probe on its own query key; detect the reply by message count + newest turn being an agent reply — no text matching.
  3. On detection, invalidate the hydration query once (guarded by isTurnInFlight()).
  4. Past the deadline, mark the turn failed via the already-exported reconcileWithServer instead of losing it.
  5. Show the waiting state via the notice slot, not isProcessing, so the composer keeps working.

Any approach that avoids these failure modes works too — happy to pair on it, thanks! :)

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

Labels

[Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants