Repository navigation
fix(dashboard): Settle stuck inline sends and report a late steer - #2428
sentry-junior[bot] wants to merge 3 commits into
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
There was a problem hiding this comment.
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.startandcommitInput→acknowledge, a poll shows history withmessage Xplusstarted [X, Y]while the mailbox still lists both.settleConversationOutboxMessagesdrops Y because ofinputMessageIds.splitInlineMailboxMessagesthen puts the server copy of Y inqueued.committedMessageIdsinConversationPage.tsxonly countsmessageevents, soPendingMailboxStackshows 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 forcommittedMessageIdstoo, so both places agree on what counts as read. That also removes the duplicate rule. The unit test callssettlealone, 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 (refetchIntervalInBackgroundis 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, butuseArchiveConversationonErrorrestores 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 inpendingMessages, and ack comes after history. - The steer error can contradict the queue.
steerWebPendingMessagesreturns no ids when the row is alreadyinterrupt, 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.
|
3 screenshot changes — 3 changed · 0 added · 0 removed Review screenshots in Frameshift
|
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>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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.

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
messageevent with the same id. Some sends never get their own Message:userMessageId = first.message.inboundMessageId). The second id is only inturn_lifecyclestarted.inputMessageIds. Two quick idle sends can both be inline.In both cases the row stayed as a pending transcript row until reload.
awaitingMessagekept the 2s poll on, andsendsInlinestayed false, so every later send went to the queue.Fix: a new
settleConversationOutboxMessagesinconversationOutbox.tsholds the cleanup rule that was inline in theuseConversationDataeffect:messageevent settles a row. Aturn_lifecyclestarted.inputMessageIdsentry 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.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
steeredInboundMessageIdswas 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
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.attemptStartInterruptIdsis computed from the leased state before resume, so steers made while paused are still eligible.steer-pending-messages.tsrepeats 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 theconversation-send+conversationsPlaywright specs (20 passed).via Aditya Mathur.
--
View Session in Sentry