Skip to content

fix(dashboard): Settle stuck inline sends and report a late steer - #2428

Open
sentry-junior[bot] wants to merge 3 commits into
mainfrom
fix/dashboard-queue-followups
Open

sentry-junior[bot] wants to merge 3 commits into
mainfrom
fix/dashboard-queue-followups

Conversation

@sentry-junior

@sentry-junior sentry-junior Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Follow-up to #2419, #2422, and #2423 after a review of the merged code.

Inline send can stay in the transcript tail

#2423 shows an idle send in the transcript and keeps the outbox row until history has a message event with the same id. Some sends never get their own Message:

  • Joined Turn input. A Turn joins contiguous sends from one author into the Message of the first one (userMessageId = first.message.inboundMessageId). The second id is only in turn_lifecycle started.inputMessageIds. Two quick idle sends can both be inline.
  • Removed from the queue elsewhere. Another participant can delete the send from their queue, or another tab can stop the Turn.

In both cases the row stayed as a pending transcript row until reload. awaitingMessage kept the 2s poll on, and sendsInline stayed false, so every later send went to the queue.

Fix: a new settleConversationOutboxMessages in conversationOutbox.ts holds the cleanup rule that was inline in the useConversationData effect:

  • A history message event settles a row. A turn_lifecycle started.inputMessageIds entry settles a row only after the mailbox no longer lists it. The Turn starts before the mailbox ack, so a joined send stays inline until then and never shows in the queue.
  • Each outbox row records when its accept arrived (acceptedAtMs). Each snapshot records when its mailbox read started (mailboxReadAtMs). The server writes the mailbox before it accepts a send, and the snapshot reads the mailbox before history. So a mailbox read that started after the accept and has neither copy means the send was removed. This also works for a background tab, a removal between polls, and an older snapshot that a failed archive restores.

Edit and steer feedback when no id comes back

Steer did nothing when steeredInboundMessageIds was empty. It now shows "That message was already read, steered, or removed." The steer API also returns no ids for a message that another tab already steered. Edit now shows "That message was already read or removed.", because cancel also returns no ids when someone else removed it.

Checked and left as is

  • Server steering (attempt-selection.ts, web-steering.ts, state.ts, worker drain scoping): a web steer joins only a web Turn of the same Actor. A Slack Turn filters to Slack records. A paused Turn drains same-author steers. attemptStartInterruptIds is computed from the leased state before resume, so steers made while paused are still eligible.
  • Draft images are lost when the composer unmounts during an edit cancel. This is by design: drafts never store image bytes.
  • The participant check in steer-pending-messages.ts repeats the one in cancel, stop, archive, and create. This is the existing pattern, so this PR does not change it.

Tests

  • tests/conversation-outbox.test.ts: queued rows, joined input while queued (including the split into queue and transcript) and after the ack, removal by accept time, and unchanged identity.
  • pnpm --filter @sentry/junior-dashboard typecheck, pnpm lint, dashboard vitest (324), and the conversation-send + conversations Playwright specs (20 passed).

via Aditya Mathur.

--

View Session in Sentry

An inline send stayed in the transcript tail when history never showed
its own Message. This happens when a Turn joins it into an earlier send,
or when another participant deletes it from the queue. The row then kept
the 2s poll on and moved every later send to the queue.

The outbox now treats Turn input ids as read. It also drops an inline row
that the mailbox listed and then lost, because the snapshot reads the
mailbox before history.

Steer now shows "Junior already read that message." when the steer
lands too late, the same as edit.

Co-Authored-By: Aditya Mathur <aditya.mathur@sentry.io>
@vercel

vercel Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
junior-docs Ready Ready Preview Oct 30, 2026 1:03pm UTC

Request Review

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

Stale Bugbot comment from a previous run.

A Turn records its input ids before the mailbox drops them. A joined send
has only its Turn input id, so dropping the inline row then let the
mailbox copy show in the queue until the next poll. Count Turn input ids
as read only after the mailbox no longer lists the send.

@sentry-junior sentry-junior Bot left a comment

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.

The mailbox-before-history rule holds up against the server. conversation-turn.ts persists the user Message and lifecycle.start (with inputMessageIds) before context.attempt.ack(). The web steering drain also persists before it acks. The detail ETag is a hash of the current response, so a 304 can't hand back older history. Joined input ids are inbound ids, which match the web messageId. Typecheck, the outbox tests, and CI (including dashboard e2e) pass. Three things before this lands:

  • A joined inline send moves into the queue for the ack window. This is the same issue Bugbot raised, and I reproduced it with the real helpers. Inline sends X and Y join into one Turn. During the window between lifecycle.start and commitInput → acknowledge, a poll shows history with message X plus started [X, Y] while the mailbox still lists both. settleConversationOutboxMessages drops Y because of inputMessageIds. splitInlineMailboxMessages then puts the server copy of Y in queued. committedMessageIds in ConversationPage.tsx only counts message events, so PendingMailboxStack shows Y as a queue row, with edit, remove, and steer actions, until the ack. At the same time, X's Message already shows Y's text. That contradicts "Nothing flashes." Fix: export the read-id rule (readMessageIds) and use it for committedMessageIds too, so both places agree on what counts as read. That also removes the duplicate rule. The unit test calls settle alone, so it can't catch this. A case that runs split + committed filtering on that snapshot would.
  • Removal elsewhere is fixed only if this tab saw the row in the mailbox first. Dropping a row needs inlineSeenQueued. A background tab doesn't poll (refetchIntervalInBackground is off), and a send could also be removed within one poll. If another participant deletes the send or another tab stops the Turn before this tab sees the mailbox list it, the row still sticks, as before. The rule also assumes cached snapshots never go backward, but useArchiveConversation onError restores a whole older detail snapshot. A sturdier rule that drops the flag: stamp each snapshot with the client time its mailbox read started. Any snapshot that started after the accept and has the id in neither the mailbox nor history means the send was removed. An accepted send is already in pendingMessages, and ack comes after history.
  • The steer error can contradict the queue. steerWebPendingMessages returns no ids when the row is already interrupt, for example after a steer from another tab with a stale cache. In that case the refetched row shows as steered while the footer says "Junior already read that message." It also returns no ids when the row was removed and never read. Consider showing the error only when the refetched mailbox no longer lists the id, or use wording that doesn't claim it was read.

@github-actions

github-actions Bot commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

3 screenshot changes — 3 changed · 0 added · 0 removed

Review screenshots in Frameshift

Conversation Child Agent Drawer · Desktop
Conversation Child Agent Drawer · Desktop
Changed
Conversation Child Agent Drawer · Mobile
Conversation Child Agent Drawer · Mobile
Changed
Conversation Share Details · Desktop
Conversation Share Details · Desktop
Changed

An inline send left the outbox only when this tab saw it in the mailbox
first. A background tab, a removal between polls, or a restored older
snapshot could still leave it stuck or drop it too early.

Each snapshot now records when its mailbox read started, and each outbox
row records when its accept arrived. A mailbox read that started after the
accept and has neither copy means the send was removed.

Edit and steer now name every reason that no id comes back, instead of
saying that Junior read the message.

Co-Authored-By: Aditya Mathur <aditya.mathur@sentry.io>

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 25d3c97. Configure here.

This branch was successfully deployed

1 active deployment
Preview – junior-docs — 25d3c972 Deployed Oct 30, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

risk: medium PR risk score: medium

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant