Repository navigation
[Bug]: chat view renders a mid-turn queued message after the reply instead of where it was sent #20846
Description
Operating system
Windows
Orca version
1.4.203 (also seen on 1.4.197)
Details
Short summary: a message typed while the agent is still working renders at the bottom of the chat view, below the whole reply it triggered — so the answer appears above the question still sitting underneath it. The terminal pane shows the same conversation in the correct order.
What happened?
Claude Code absorbs a mid-turn send into the running turn and writes no type:"user" row for it. The optimistic echo therefore has no transcript row to match — ever — and stays pinned at the tail of the list while the rest of the reply lands above it.
Captured from ~/.claude/projects/<encoded-cwd>/<session>.jsonl, line numbers as they appear in the file:
6 {"type":"user", ...} "check status, first time"
25 {"type":"assistant", ...} "I'll check the current state..."
27 {"type":"queue-operation","operation":"enqueue","content":"check status, second time"}
28 {"type":"queue-operation","operation":"remove","content":"check status, second time",
"reason":"absorbed_mid_turn"}
33 {"type":"attachment","attachment":{"type":"queued_command",
"prompt":"check status, second time"}}
41 {"type":"assistant", ...} "Working tree is clean and in sync with origin..."
61 {"type":"assistant", ...} "It's 14:20 UTC - both armed probes have fired..."
68 {"type":"assistant", ...} "The discriminating test has fired..."
The echo belongs at line 33. The chat view renders it after line 68.
| order shown | |
|---|---|
| Terminal pane (correct) | msg 1 → tools → msg 2 → more tools → final answer |
| Chat view (wrong) | msg 1 → tools → final answer → msg 2 |
How can we reproduce it?
- Open an agent pane running Claude Code and send a prompt that takes ~30s of tool calls.
- While it is still working, send a second, clearly different message.
- Compare the terminal pane with the chat view.
Anything else that might help:
Mobile already fixes exactly this, and its test says so in as many words (mobile/src/session/mobile-native-chat-render-data.test.ts):
Claude consumes a mid-turn send through a
queued_commandattachment and writes notype:"user"record for it, so that echo has no row to match - ever. What made the conversation read as scrambled was not the unmatched echo itself but where it rendered: appended after every turn, so it re-read below each new one.
Mobile anchors on baselineTailMessageId. Desktop records the same boundary as NativeChatPendingSend.afterMessageId but only uses it for matching — pendingSendsAsMessages deliberately timestamps echoes to sort to the end:
the send time as the timestamp so they sort to the end (most recent) of the list
A second path reaches the same symptom: a message sent to a pane whose agent session is not yet registered is never delivered and likewise produces no user row, so its echo dangles at the tail too.
Workaround: wait for the turn to finish before sending the next message.
A PR follows.
Activity
AmethystLiang commentedon Sep 15, 2026
thanks for reporting. looking into it
lurunzi commentedon Sep 22, 2026
Independent Windows report with Claude Code 2.1.278: messages sent during an active reply were received by Claude, but remained at the bottom of Orca Chat as pending echoes. Read-only inspection found
queue-operation.removewithreason: absorbed_mid_turn, followed by a persisted attachment with this sanitized shape:{"type":"attachment","uuid":"synthetic-message","timestamp":"2026-09-22T19:37:59.013Z","attachment":{"type":"queued_command","prompt":"Check the result while you continue.","commandMode":"prompt","origin":{"kind":"human"},"humanTurn":true}}The shared Claude decoder drops this record on inspected main
18696bd875. A narrowly scoped personal-fork fix decodes this positively identified human prompt as an ordinary transcript user message, letting existing pending reconciliation remove its echo without resending or introducing a turn boundary:This is evidence for the persisted-human-attachment case, not a complete fix for every case in this issue. The narrow filter intentionally excludes older records without
origin.kind/humanTurn; it does not solve general file-order versus timestamp ordering or prompts that never reached the process. Existing PRs #14309, #16121, #19428 and #20847 already address overlapping scopes, so I am adding the evidence here rather than opening another competing PR. No user transcript, paths, project names or images are attached.topnguyen commentedon Sep 23, 2026
Thanks — this is the piece I was missing.
queue-operation.removewithreason: absorbed_mid_turnfollowed by thesynthetic-message/queued_commandattachment explains why there is no row to match: the shared decoder on main still drops it. I can confirm independently thatsrc/main/native-chat/transcript-line-decoders-claude.tshas had no commits onmainsince this issue was filed, so nothing has landed for it yet.Worth stating plainly for anyone triaging: our two fixes sit at different layers and compose rather than compete.
The second case survives the first by your own scoping: records without
origin.kind/humanTurn, and prompts that never reached the process, still leave an echo with nothing to reconcile against. Held at the tail it re-reads below every turn of the reply it triggered, which is the symptom in the title here.#19428 already carries a staff version of the admission path and has been sitting in draft since 8 Sep, so your evidence is probably best aimed there rather than at a fifth PR — I have linked it from that thread.
hishiki1029 commentedon Oct 11, 2026
Also reproduced on macOS (Orca 1.4.224, Claude Code agent).
Two additional observations:
The terminal pane shows the correct order, same as described above.
fix(native-chat): place mid-turn and `!` prompts where they were sent
wutonglab commentedon Oct 17, 2026
Still reproducible on Orca 1.4.224 (Linux server + Orca mobile) with Claude Code terminal sessions, and the cause is entirely on the Orca side, so here is a minimal fix that we have tested locally.
Data. One long Claude session has 408
queue-operation remove reason:absorbed_mid_turnrecords and 406attachment.type:"queued_command"records, and none of them has atype:"user"row with the same text. Of those attachments, 53 are human prompts (commandMode:"prompt",origin.kind:"human"); the rest are task notifications.Mechanism (1.4.224 bundle).
transcript-line-decoders-claude.ts, used bynativeChat.subscribe) returnsnullfor every record whosetypeis notuser/assistant, so thequeued_commandattachment never reaches clients.role:userrow with the same normalized text appears. It never appears, and there is no timeout.baselineTailMessageId. Once the 40-row subscription window slides past that anchor, the echo falls into the unanchored bucket and is appended after everything, including the streaming row, which is the "stuck at the bottom" symptom.The echo lives only in in-memory React state, so reloading the app clears it. After that, the absorbed prompt is simply missing from chat. A next normal turn, the end of the turn, or toggling the chat view does not clear it. A related side effect is that prompts typed mid-turn in the desktop terminal never show up in chat on any device.
Fix. In the Claude decoder, map
type:"attachment"withattachment.type === "queued_command",commandMode === "prompt", andorigineither absent or{kind:"human"}(and not sidechain or meta) to arole:"user"text message. Use the attachment'suuidas the id and keep itsparentUuidandtimestamp. Keep dropping task notifications, peer messages, and other attachments. The row lands at the transcript position where Claude actually absorbed the prompt, and the existing text-match logic then retires the optimistic echo. Turn-state detection is unaffected because it only looks atuser/assistantrecords. Happy to open a PR with tests if that direction works for you.wutonglab commentedon Oct 18, 2026
Two more findings on the phone chat send path, reproduced on Orca 1.4.224 (Linux host, Claude Code 2.1.302). I reproduced them at the host RPC level: I replayed the exact
terminal.sendcalls the 1.4.224 mobile client makes for a plain Claude message (first{text:"\x15", enter:false}, then{text, enter:true}with noagentPrompt). I did not drive the phone UI itself.Path. These calls skip the desktop agent-prompt branch, because the
terminal.sendhandler (src/main/runtime/rpc/methods/terminal/terminal-send-method.ts) only takes that branch whenagentPrompt && client.type === 'desktop'. They go tosendTerminal→writeActionWithinLimit(src/main/runtime/runtime-terminal-writer.ts), which writes the text, waits about 500 ms, then writes a separate\r. That path has no permission/selector guard, no render gate and no turn_started check, unlikewriteTerminalAgentPrompt(src/main/runtime/terminal/input/terminal-agent-prompt.ts).1. An unsent draft in the agent's input box gets submitted with the message.
The mobile client clears the input with a single Ctrl+U (
clearMobileNativeChatInputwritingAGENT_TUI_CLEAR_INPUT_LINE). Ctrl+U only deletes from the cursor back to the start of the current logical line."MOBILE-A: say hi", as expected.draft line one\ndraft line two: the transcript user row is"draft line one\nMOBILE-B: say hi".please reply with the word, cursor moved 4 to the left: the transcript user row is"MOBILE-C: say hiword".Expected: either exactly the sent text is submitted, or the send is refused with "input has unsent text". Actual: leftover draft text is merged into the prompt and silently changes what the agent receives, and the rest of the draft is lost. Not verified on the phone: whether the optimistic bubble retires when the row is draft + message rather than the message alone.
2. A message sent while Claude shows a selector answers the selector.
Steps: get Claude to call AskUserQuestion (options Red/Green/Blue, Red highlighted), then replay the mobile send
"MOBILE-D: what is 2+2".Actual: the selector closes with
→ Red. The transcript has atool_resultof"Red", and the message text appears nowhere.terminal.sendreturnedaccepted: true, so the phone would show it as sent.Contrast: with the same selector open,
orca terminal send --text … --enter, which is the desktopagentPromptpath, is refused withagent_prompt_blocked, and the selector stays untouched.Since #25761 the phone disables Send while a prompt card is shown. The host still accepts these writes, though, so a send gets through whenever the card is not detected yet or Send has been re-enabled with the collapse chevron.
Suggested fixes
text+enterpath the same way as the agent-prompt path. Refuse with arefusedReasonwhile the agent is in permission/question state (assertAgentPromptPermissionSafe). Alternatively, let mobile clients use theagentPromptbranch, which also gives the render gate and turn_started confirmation, so the phone and desktop share one submit path.terminal.read --screenalready exposes it asdraft). If it is not empty, either clear all of it (theconfirmCleared+ widened burst used for launch drafts, applied to any draft, plus moving the cursor to the end first) or refuse the send and show "Message not sent – input has unsent text".accepted: true.