Skip to content

fix(client): reconcile inbox rows after missed delivery events - #57427

Closed
aniruddhaadak80 wants to merge 1 commit into
anomalyco:devfrom
aniruddhaadak80:fix/tui-stale-queued-card
Closed

aniruddhaadak80 wants to merge 1 commit into
anomalyco:devfrom
aniruddhaadak80:fix/tui-stale-queued-card

Conversation

@aniruddhaadak80

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #56748

Type of change

  • Bug fix

What does this PR do?

A queued user prompt could stay displayed above the prompt input after it had been delivered, and clicking it returned 409 because the server-side inbox row was already gone.

The root cause is the optimistic-admission guard. When a prompt is sent, the client admits it locally and records its ID in outbox. That guard is meant to keep the optimistic row through an inbox read that may race the admission. It was cleared by a positive inbox read, the session.inbox.enqueued echo, or a rollback — but not by a successful session.prompt response.

If the input was delivered before the echo (or the enqueue/deliver events were missed across a reconnect), the ID stayed in outbox forever. Every later inbox snapshot then treated the local row as still in flight, even though the server had removed it. The row was pinned until restart, which is exactly what the report describes. Image prompts make the window wider because admission takes longer, but any prompt can hit it.

This change:

  • Clears the guard when the admission POST succeeds — the response returns the durably admitted inbox item, so the optimistic window is over.
  • Clears the guard when an item is delivered or cancelled.
  • Re-reads the authoritative inbox at execution turn boundaries and after a failed inbox action, so a missed delivery event (or a 409 from a stale card) reconciles instead of leaving the card on screen.

How did you verify your code works?

New regression test in packages/client/test/solid-inbox-race.test.ts: admission succeeds, the delivered event is never emitted, and the next inbox snapshot is empty. Without the fix the row stays pending (Received: true); with it the row is gone.

  • packages/client: 230 pass, 4 skip, 0 fail (bun test --timeout 30000)
  • bun run typecheck passes in packages/client and packages/tui
  • oxlint reports 0 warnings/errors on the changed files

I could not reproduce the original Windows report directly, but the test pins the stale-state mechanism and the UI path now reconciles on the 409.

Screenshots / recordings

Not a UI change. (The behavior change removes a stale card.)

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

A prompt's optimistic row was guarded by its `outbox` entry until the server
echoed `session.inbox.enqueued`. If the input was delivered before that echo,
or the enqueue/deliver events were missed across a reconnect, the guard never
cleared and every inbox snapshot kept the row as "inflight" even though the
server had already promoted it. The queued card then stayed above the prompt
until restart, and acting on it returned 409 because the server row was gone.

Clear the optimistic guard when the admission POST succeeds (a durable
acknowledgement), on delivery/cancellation, and refresh the authoritative
inbox at turn boundaries and after a failed inbox action. Add a regression
test where admission succeeds but the delivered event is lost.
Copilot AI balanced review requested due to automatic review settings October 20, 2026 14:59

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

@opencode-agent opencode-agent 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.

Looks good. I ran the #56748 reproduction with opencode-drive: it drops the TUI's connections right before sending a prompt. On the base, the delivered prompt stays as a pending card after idle. With this change it shows as a normal delivered message (screenshots below). The new regression test fails on the base and passes here. The packages/client suite passes (230 pass, 0 fail), and typecheck is clean in packages/client and packages/tui.

Clearing the outbox guard once the admission POST succeeds matches what compact() already does. One small side effect: an inbox read that started before the POST and finishes after it, but before the session.inbox.enqueued event, can briefly drop the pending card. The transcript message stays, and the event puts the card back, so I don't think that blocks this.

Public API is unchanged.

Before

After

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@SKar-2007

Copy link
Copy Markdown

@aniruddhaadak80 — heads up: this closed about a minute before the bot's validation review posted, and that review is positive — it reproduced #56748 on base with opencode-drive, confirmed this branch clears the stale card, and the new regression test fails on base / passes here.

If you're not planning to reopen it, I've taken the fix forward in #58072 with credit to your diagnosis. One part of this branch doesn't survive the existing test suite, fwiw: clearing the outbox guard when the prompt POST settles fails the pinned "keeps the row when the response lands before the echo" test in packages/tui (inbox reads can lag that response), so #58072 drops the guard on delivered-message evidence and the 409 conflict instead. Happy to talk through it if you'd rather continue there.

SKar-2007 added a commit to SKar-2007/opencode that referenced this pull request Oct 22, 2026
A prompt admitted optimistically is pinned by the outbox guard until its inbox echo, a positive inbox read, or a rollback clears it. When the event stream drops across both the enqueued and delivered echoes, no inbox read can prove the row consumed — an empty snapshot is also what a lagging projection looks like — so the queued card outlived its turn until restart, and clicking it hit a 409.\n\nDrop the guard from evidence instead. A fetched message proves delivery: the projection writes the message in the same transaction that consumes the inbox row, so message.sync removes the matching pending row. A lifecycle conflict proves the row is not in the expected state; the session route releases the guard and re-reads the inbox, so a consumed row clears while one that already has the requested delivery stays. A successful cancel is authoritative on its own and discards the row; removePending clears the outbox entry for every removal path.\n\nClearing the guard when the prompt POST settles, as in anomalyco#57427, breaks the pinned invariant that reads may lag that response (test/cli/tui/data.test.tsx 'keeps the row when the response lands before the echo').
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tui: queued card for a delivered image message lingers above the prompt input

3 participants