Skip to content

fix(playground): clear stale pending-signal badge in Studio chat#17682

Open
aisensiy wants to merge 1 commit into
mastra-ai:mainfrom
aisensiy:fix/studio-pending-signal-echo-race
Open

fix(playground): clear stale pending-signal badge in Studio chat#17682
aisensiy wants to merge 1 commit into
mastra-ai:mainfrom
aisensiy:fix/studio-pending-signal-echo-race

Conversation

@aisensiy

@aisensiy aisensiy commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Closes #17681

Summary

Studio chat's pending: … indicator above the message input could linger after the agent finished replying — and pile up across messages — clearing only on a page refresh.

Root cause

The badge is added via onSignalSent (the send-message HTTP response) and removed via onSignalEcho (the thread-subscription stream) — two async channels with no ordering guarantee. On the idle path the server starts the run and emits the data-user-message echo over the already-open subscription before the send-message response returns, so onSignalEcho fired before onSignalSent had added the pending entry: the remove ran as a no-op, then the late add orphaned a badge that no further echo would ever clear. (The echo itself arrives fine — the user message renders in history — only the badge is stranded.)

Fix

Make the pending bookkeeping order-independent using two synchronous id mirrors, so add/remove are commutative regardless of arrival order:

  • pendingSignalIdsRef — mirrors the currently-visible badge ids
  • prematurelyEchoedSignalIdsRef — remembers echoes that landed before their add, so the late add is skipped instead of resurrecting a badge

Both are routed through a clearPendingSignals() helper at the three reset sites (thread/agent switch, onThreadSignalsUnsupported, onCancel).

Testing

  • New deterministic unit tests force the echo-before-add ordering and assert the badge clears (fails before the fix, passes after), plus normal-order and per-id independence cases.
  • Validated live in a dev Studio against a real agent: the lingering pending no longer appears.
  • pnpm --filter ./packages/playground test src/services → 54/54, plus typecheck and eslint pass.

Review follow-ups included here

  • Add afterEach(cleanup) to the provider test — this package runs vitest with globals disabled, so React Testing Library's auto-cleanup never registers and rendered providers were leaking between tests; also hardened getSignalCallbacks.
  • Reuse the exported PendingSignalMessage type instead of an inline literal.

Known follow-ups (not in this PR)

A fully-robust fix for two related, lower-severity edges — a narrow reset-vs-late-send re-race, and unbounded growth of the premature-echo set from foreign/duplicate echoes on a shared thread — belongs at the @mastra/react useChat SDK boundary, where the ordering hazard originates (both callbacks fire from inside the hook). Tracking separately rather than band-aiding per consumer.

🤖 Generated with Claude Code

ELI5

When you send a message in Studio chat, a "pending: ..." badge appears while the message is being processed. This badge should disappear once the server confirms the message was received, but sometimes it sticks around and won't go away even after you get a reply—you have to refresh the page to clear it. This happens because the confirmation and the acknowledgment can arrive out of order, and the code only knew how to clean up if they arrived in the right order.

Summary

This PR fixes a race condition in Studio's chat pending signal indicator where the pending: … badge above the message input could linger after the agent replied and accumulate across messages.

Root cause: The pending badge is added when the send-message HTTP response arrives (onSignalSent) and removed when the echo is received from the thread-subscription stream (onSignalEcho). Since these two async channels have no ordering guarantee, when an echo arrives before the send response completes, the premature remove becomes a no-op. A subsequent add then leaves an orphaned badge that never clears.

Solution: The provider now maintains order-independent bookkeeping using two synchronous Set refs:

  • pendingSignalIdsRef: tracks currently-visible pending badge IDs
  • prematurelyEchoedSignalIdsRef: tracks echoes that arrived before their corresponding send confirmation

A new clearPendingSignals() helper centralizes cleanup logic and ensures both refs are cleared at reset points (thread/agent switch, losing signal support, cancelling a run). The logic skips late adds if an echo was already seen for that signal ID, making the add/remove operations commutative regardless of arrival order.

Changes:

  • mastra-runtime-provider.tsx: Added the two ref-based tracking sets and clearPendingSignals() helper; updated thread/agent switch, signal-unsupported handler, and cancel handlers to use centralized cleanup
  • mastra-runtime-provider.test.tsx: Added cleanup() between tests to prevent state leakage; added three new unit tests validating signal lifecycle under normal ordering, reverse ordering (echo-before-send), and per-ID independence
  • .changeset/studio-pending-signal-badge.md: Documents the patch release for @internal/playground

Testing: Deterministic unit tests now force the problematic echo-before-add ordering (previously failed, now pass), plus normal-order and per-id independence cases; validated live in dev Studio; package tests and linters pass.

The "pending: …" indicator above the chat input could linger after the
agent finished replying (and pile up across messages), clearing only on a
page refresh. The badge is added via onSignalSent (the send-message HTTP
response) and removed via onSignalEcho (the thread-subscription stream) —
two async channels with no ordering guarantee. On the idle path the echo
arrives before the add, so the remove ran as a no-op and the late add
orphaned a badge that no further echo would ever clear.

Make the bookkeeping order-independent using synchronous id mirrors
(pendingSignalIdsRef + prematurelyEchoedSignalIdsRef) so add and remove are
commutative, routed through a clearPendingSignals() helper at the reset
sites.

Also from review follow-up: add afterEach(cleanup) to the provider test
(this package runs vitest with globals disabled, so React Testing Library's
auto-cleanup never registers and rendered providers leaked between tests),
harden getSignalCallbacks, and reuse the exported PendingSignalMessage type.

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

vercel Bot commented Jun 8, 2026

Copy link
Copy Markdown

@aisensiy is attempting to deploy a commit to the Mastra Team on Vercel.

A member of the Team first needs to authorize it.

@changeset-bot

changeset-bot Bot commented Jun 8, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: d85a977

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@internal/playground Patch
mastra Patch
create-mastra Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR fixes a race condition where "pending" signal badges linger in Studio chat when the server's signal echo arrives before the send confirmation. The fix introduces commutative ref-based tracking in MastraRuntimeProvider to handle out-of-order callbacks, adds comprehensive test coverage for signal lifecycle scenarios, and documents the change.

Changes

Pending signal race-condition fix

Layer / File(s) Summary
Race-safe pending signal state and lifecycle handling
packages/playground/src/services/mastra-runtime-provider.tsx
Adds PendingSignalMessage type and introduces two refs (pendingSignalIdsRef, prematurelyEchoedSignalIdsRef) to track pending vs. prematurely-echoed signal IDs. Implements commutative add/remove logic in addPendingSignal and removePendingSignal so that signal badge clearing works correctly regardless of callback order. A new clearPendingSignals() helper is called on thread/agent switches, when signals become unsupported, and during cancellation.
Signal lifecycle test coverage
packages/playground/src/services/__tests__/mastra-runtime-provider.test.tsx
Adds cleanup import and an afterEach hook to prevent leaked renders between tests. Introduces a getSignalCallbacks() helper to extract the latest onSignalSent and onSignalEcho callbacks from the provider. Implements three async tests: normal send→echo flow, race condition (echo arrives before send), and per-signal-id isolation (multiple unrelated signals).
Changeset documentation
.changeset/studio-pending-signal-badge.md
Documents the @internal/playground patch release and describes the fix for stale pending signal badges in Studio chat.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • mastra-ai/mastra#17312: Changes when thread signals are enabled/disabled via chatWithLegacyStream, which affects the clearPendingSignals cleanup path added in this PR.
  • mastra-ai/mastra#17238: Modifies the React useChat signal-sending flow to track echoed vs. resolved signal IDs, which directly feeds the pending-signal lifecycle that this PR fixes.

Suggested labels

tests: green ✅, complexity: high

Suggested reviewers

  • TylerBarnes
  • abhiaiyer91
  • taofeeq-deru
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main fix: clearing stale pending-signal badges in Studio chat. It is concise (64 chars), uses imperative mood, proper capitalization, and directly maps to the changeset.
Linked Issues check ✅ Passed The PR fully addresses #17681 by implementing order-independent bookkeeping with pendingSignalIdsRef and prematurelyEchoedSignalIdsRef to prevent lingering badges regardless of onSignalSent/onSignalEcho ordering.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the pending signal badge issue: test setup, signal lifecycle tests, and race-safe state management in MastraRuntimeProvider with no unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@dane-ai-mastra dane-ai-mastra Bot added the complexity: low Low-complexity PR label Jun 8, 2026
@dane-ai-mastra

dane-ai-mastra Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

PR triage

Linked issue check passed (#17681).

Mastra uses CodeRabbit for automated code reviews. Please address all feedback from CodeRabbit by either making changes to your PR or leaving a comment explaining why you disagree with the feedback. Since CodeRabbit is an AI, it may occasionally provide incorrect feedback.


PR complexity score

Factor Value Score impact
Files changed 3 +6
Lines changed 138 +6
Author merged PRs 1 -1
Test files changed Yes -10
Final score 1

Applied label: complexity: low


Changed test gate

Changed Test Gate is pending. The Changed Test Gate / changed-tests check will update the test label when it completes.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
packages/playground/src/services/__tests__/mastra-runtime-provider.test.tsx (1)

237-319: 🏗️ Heavy lift

potential_issue: New signal lifecycle tests continue the mocked-callback pattern instead of the package’s MSW-first integration path.

These cases validate onSignalSent/onSignalEcho via mocked useChat internals rather than exercising the real @mastra/client-js + React Query flow with network-only mocking.

As per coding guidelines: packages/playground/**/*.test.{ts,tsx} should use the MSW-first strategy and “never mock playground’s own data hooks, services, or auth gating with vi.mock.”

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1567e557-eaf7-4f7d-a5d3-a066c6ccd66a

📥 Commits

Reviewing files that changed from the base of the PR and between 8377e5a and d85a977.

📒 Files selected for processing (3)
  • .changeset/studio-pending-signal-badge.md
  • packages/playground/src/services/__tests__/mastra-runtime-provider.test.tsx
  • packages/playground/src/services/mastra-runtime-provider.tsx

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

Labels

complexity: low Low-complexity PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] Studio chat: 'pending' signal indicator lingers above the composer after the agent replies

1 participant