Skip to content

fix(native-chat): show prompts Claude queued while the agent was busy - #14309

Closed
lulumeya wants to merge 6 commits into
stablyai:mainfrom
lulumeya:fix/native-chat-queued-prompt-visibility
Closed

lulumeya wants to merge 6 commits into
stablyai:mainfrom
lulumeya:fix/native-chat-queued-prompt-visibility

Conversation

@lulumeya

@lulumeya lulumeya commented Aug 13, 2026 •

Copy link
Copy Markdown

Still needed after #16117

#16117 (fix(native-chat): anchor an unmatched chat echo where it was sent, merged 2026-08-27) diagnosed the same root cause this PR fixes: Claude consumes a mid-turn send through a queued_command attachment and writes no type:"user" record — 1,049 of 1,132 human queued sends in that PR's measurement. It concluded "no amount of better matching changes that" and fixed the placement of the unmatched echo bubble. This PR makes the queued record decode into a real user message in the transcript read. Three reasons #16117 does not cover it, each verified:

  1. fix(native-chat): anchor an unmatched chat echo where it was sent #16117 is mobile-only. All four of its files are under mobile/; it touches zero files under src/. The desktop chat pane is untouched — on desktop the prompt is still absent entirely, not merely misplaced.

  2. The echo is memory-only. native-chat-pending.ts holds pending sends in const pendingSendCache = new Map<...>(). fix(native-chat): anchor an unmatched chat echo where it was sent #16117 keeps that in-flight bubble in the right position, but the bubble does not exist after a reload, on another client, or for anyone reading the session later. This PR puts the prompt into the transcript read itself, so it is durable.

  3. The gap is still open on main. Current main (df14d1a29) has no queued_command handling and no attachment decoding anywhere under src/main/native-chat/. Running the production reader on current main over the synthetic 13-record fixture that ships with this PR:

    [main] rendered texts: ["THINKING_1","ASSISTANT_TEXT_1","","","THINKING_2","ASSISTANT_TEXT_2"]
    [main] queued prompt present? false
    

    The mid-turn prompt is missing. With this PR the same fixture renders it between the turns that bracket it. (The harness for this check was a throwaway test importing only main's readNativeChatTranscript; it is not part of the diff.)

The two changes are complementary and do not conflict: this PR touches only src/ and tests/, #16117 only mobile/. Once a mid-turn prompt decodes into a real user row, echoes for it become matchable normally, and #16117's anchoring becomes the fallback rather than the only answer.

ELI5

Send a prompt while the agent is mid-turn and, in the desktop chat pane today, it never appears — the terminal shows it, the agent answers it, chat has no trace it was ever said. The mechanism: when you message a coworker who's busy, they finish what they're doing, then read your message. Claude Code does the same — a mid-turn prompt gets queued, and its transcript record is stamped when you sent it but written to the file when the agent finally took it. Chat did two wrong things with that record: it threw it away entirely, and — once taught to read it — sorted it by its stamp, placing it before a reply the terminal showed it after. This PR decodes the queue record as a normal user message and pins it to the spot the transcript file gave it, so chat and terminal tell the same story. A second visible fix falls out of the same change: the temporary echo bubble chat shows the instant you hit send used to linger forever at the bottom after a mid-turn send — no real message ever arrived to confirm it — and now retires normally.

What Changed

Where to look first: the whole change is two small pieces — everything else is wiring and tests.

File What it does
src/main/native-chat/transcript-line-decoders-claude.ts 🔍 Start here. Decodes queued_command attachments into role:'user' messages
src/main/native-chat/queued-prompt-file-order.ts 🔍 Then here. Anchors a queued prompt's timestamp to its file position
src/main/native-chat/transcript-reader.ts / transcript-tail-reader.ts / transcript-watch-engine.ts Wires the anchoring pass into every seam chat reads through
src/shared/native-chat-types.ts New optional queued flag on NativeChatMessage
queued-prompt-file-order.test.ts / transcript-line-decoders.claude.test.ts / transcript-watch-queued-prompt.test.ts Unit + live-tail regression coverage
src/renderer/src/components/native-chat/native-chat-pending.test.ts Characterization tests: the composer's optimistic echo retires once the queued user turn lands (behaviour only, no production change)
tests/e2e/native-chat-queued-prompt-visible.spec.ts Hermetic e2e proving the prompt renders once, in file order

1 — Decode the record instead of dropping it.

// BEFORE — anything not user/assistant fell through:
const role = record.type
if (role !== 'user' && role !== 'assistant') {
  return null // ← queued prompts died here
}
// AFTER — attachment records get their own decoder:
if (record.type === 'attachment') {
  return decodeQueuedCommandAttachment(record, fallbackId) // → role:'user', queued: true
}

Content goes through claudeContentBlocks, the same path ordinary turns take, so image-carrying queued prompts decode too. Restricted to commandMode === 'prompt' (see Why).

2 — Anchor it to file order. anchorQueuedPromptsToFileOrder walks a batch in file order; when a queued prompt's timestamp precedes its predecessor's, it restamps to predecessor + 1. One tick, not equal — compareMessages breaks timestamp ties on id lexicographically, so an equal stamp would sort it right back up.

3 — Apply it at every seam chat reads through: readNativeChatTranscriptTailFile (initial snapshot, replacement, pagination), readNativeChatTranscript, and the watch engine's append emit. The watch engine carries the predecessor timestamp across reads (queuedAnchorTimestamp) and resets it on file replacement — a live append batch can contain only the queued record, leaving nothing in-batch to anchor against.

Why

Claude stamps the queue record at enqueue time but appends it once the queue drains, so timestamp and file position disagree — and the queue entry is almost always the only record the prompt ever gets (no type:"user" line is written for it):

sequenceDiagram
    participant T as Terminal
    participant C as Claude Code
    participant F as transcript.jsonl
    T->>C: prompt B (agent mid-turn)
    Note over C: B queued — record stamped NOW (enqueue time)
    C->>F: append reply to prompt A (newer timestamp)
    C->>F: append B's queued record (older timestamp)
    Note over F: file order: reply, B — time order: B, reply ⚠️
Loading

Time-sorting trusts the stamp; the terminal and the agent followed file order. Anchoring keeps the position the file already assigned, which is the order the user actually experienced.

Why the commandMode filter: across real transcripts, commandMode: 'prompt' is a typed prompt and task-notification is a harness-generated XML blob the agent queues for itself — roughly half (~46%) of all queued_command records, and no other values exist. The orchestration transcript path (src/main/runtime/orchestration/worker-transcript-read.ts → boundWorkerTranscriptMessages) has no stripNoiseMessages and a 40-message window, so without the filter a supervising agent would read harness XML as human input.

A second fix that falls out: the stranded composer echo. Sending from the chat composer immediately renders an optimistic "queued" echo bubble so the send feels instant. prunePendingSends (src/renderer/src/components/native-chat/native-chat-pending.ts) retires that echo via advancedNativeChatUserContentCounts (native-chat-pending-occurrence.ts), which tallies role: 'user' messages by normalized content key and promotes a tally only once a non-user turn follows it. Before this PR a mid-turn prompt produced no user record at all, so its content key never appeared, the advanced count stayed at zero, and the echo was kept indefinitely — and because pendingSendsAsMessages stamps the echo with the local send time, it sorted to the end of the list, sinking below turns that landed after it instead of holding its position. With this PR the decoder emits that prompt as a real role:'user' turn anchored between its bracketing turns, the following assistant turn advances the count, and the echo retires normally. The counter filters on neither source nor the queued flag, so no renderer change was needed. This is the same stranded echo #16117 anchors on mobile; here it retires because a real user row now lands. Found by @grboy6770 while independently replaying a redacted production transcript against the branch; verified by reading the code path and pinned by two characterization tests (see Testing) — not exercised in a running app.

Blast radius — verified by reading each call site, not assumed

attachment records previously always decoded to null, so the decode change is structurally "was null → now a message". Checked against everything downstream of the shared decoder:

  • Turn lifecycle (transcript-turn-lifecycle.ts) filters to user/assistant before the shared decoder — unaffected.
  • AI-Vault scanners use their own parsers — unaffected.
  • Double processing impossible: the anchoring pass is idempotent — a second pass changes nothing and returns the same array reference — so the two entry points cannot compound.
  • /clear boundaries, id/turnId dedup, tail-reader offsets: safe. turnId is intentionally unset, matching all four sibling decoders.
  • isSidechain: true queued commands: none exist in the data, so subagent chatter cannot leak in.
  • Provider neutrality: the decoder change is Claude-only; the ordering pass is a no-op for agents that never mark a message queued.

Known limitations, stated honestly:

  1. The +1 tick could collide if the following record's timestamp were within 1ms of the predecessor's. Simulated over 2,407 local transcripts: 0 collisions; 866 of 881 queued prompts restamped, median shift 17.5s.
  2. Rare double render. If the same prompt is also re-recorded as a real user line (~0.45% in local data), both survive — the assembler deliberately refuses to merge two same-source prompts with identical text, so genuine repeats are preserved.
  3. The 40-record mid-batch emit path inside the incremental reader is covered by the same anchoring call but is not separately tested.

Linked Issue

Fixes #14308

Visual Proof

🔴 BEFORE — chat is missing the queued prompt the terminal shows Before — the queued prompt is absent from chat

Order on screen: start the long running task → working through the first task now → both done. The queued prompt (and check the config while you are at it) is nowhere, even though the agent answered it.

🟢 AFTER — same transcript; the prompt is present, in the order the terminal showed After — the queued prompt is present, after the reply it followed

Order on screen: start the long running task → working through the first task now → and check the config while you are at it → both done. The prompt sits after the reply it was appended behind — the same order the terminal showed.

Branch State

  • Rebased onto main at df14d1a29; branch head 1710575bb; 0 commits behind; GitHub reports MERGEABLE.
  • The rebase had one conflicted file, transcript-watch-engine.ts; both conflicts were import blocks only — upstream added imports where this branch also adds one. No logic conflict.
  • Size: +143 / −6 production code across 7 files; +741 tests and fixtures across 8 files.
  • CI has never run for this PR (first-time-contributor workflow gate), so nothing below is verified by the project's own pipeline — all gates are local runs after the rebase.

Testing

Gate Result
pnpm typecheck / pnpm lint ✅ clean
Build (electron-vite build, both desktop targets) ✅ exit 0
Unit — native-chat suites ✅ 1,628 tests across 179 files pass
Page-boundary regression (transcript-tail-reader-queued-page.test.ts) ✅ a window that starts at the queued record still anchors it against a predecessor outside the page; mutation-checked — anchoring after the slice fails both cases
Live-tail regression (transcript-watch-queued-prompt.test.ts) ✅ subscribes to a real temp transcript, appends a reply, then appends the queued record alone; asserts it sorts after the reply rather than back at enqueue time
E2e (tests/e2e/native-chat-queued-prompt-visible.spec.ts) ✅ hermetic, no Claude CLI; fixture places an assistant turn between the queued prompt and its reply; asserts the prompt renders exactly once and all four texts appear in file order. Verified failing without the fix (element(s) not found), passing with it
Echo-retirement characterization tests (native-chat-pending.test.ts) ✅ one case keeps the composer echo when no user row lands, one retires it once the queued user turn is present; removing the queued row from the second case fails it, so the pair is not vacuous. Behaviour pinned by code reading and unit tests only — not verified in a running app
Contributed replay (src/main/native-chat/queued-prompt-trace-replay.test.ts) ✅ 4 cases driving the production reader over a synthetic 13-record trace, contributed by @grboy6770: the prompt lands between its bracketing turns, arrives once as a queued user turn, stays out when the attachment is self-queued, and behaves the same when records arrive while the pane is open (incremental path). 3 of the 4 fail without the fix — verified on the pre-rebase base, and the check on current main at the top of this description shows the same gap
Mutation checks (×2) ✅ disabling the commandMode guard fails exactly the task-notification test; disabling live-append anchoring fails exactly the new live-tail test — both guards genuinely covered, not incidentally passing
Full suite, vitest run directly with the same command on both ⚠️ clean main (df14d1a29): 66,720 passed / 44 failed in 16 files. This branch: 66,743 passed / 48 failed in 18 files. The three files failing only on this branch — transcript-watch.test.ts, run-electron-vite-dev.test.ts, relay/subprocess.test.ts — each pass in isolation (28, 6, 31 tests), and clean main has one failure this branch does not (git-admission-storm-measurement.test.ts). Load-dependent flakiness on both sides; this PR introduces no failure, but neither run is green
Project CI ⚠️ never run — awaiting first-time-contributor workflow approval
Real dev build ✅ confirmed by the reporting user: chat and terminal now show the same order
Platforms macOS only

pnpm test itself could not start locally — main now pins pnpm@12.0.0 and the local toolchain could not switch to it — so vitest was invoked directly.

Three independent code review passes ran before this PR (architecture, TypeScript/runtime, ordering); findings are folded in — including one blocker, where the first ordering fix missed the live-append path entirely.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

AI Disclosure

Claude Opus 5 (Claude Code) investigated the bug, wrote the fix, the unit and live-tail tests, and the e2e spec.

Review

  • Security: no new input surface — the same local transcript file, and queued-prompt content flows through claudeContentBlocks, the exact path ordinary user turns already take.
  • Cross-platform: pure TypeScript over the existing transcript-read abstractions; no paths, shortcuts, or platform branches. Runtime-tested on macOS only.
  • Remote SSH / wire: no new opcode. The queued flag is a new optional field (safe per docs/reference/remote-wire-compatibility.md), documented as main-process-ordering only — renderers must not branch on it.
  • Mobile: works — the tail reader anchors the whole chronological read before windowing, so prompts at a page edge anchor too (regression-tested above). Touches no mobile/ files, so no overlap with fix(native-chat): anchor an unmatched chat echo where it was sent #16117.
  • Backwards compatibility: strictly additive — records that previously decoded to null now decode to a message; nothing that decoded before decodes differently.
  • Performance: the new decoder branch runs only for type === 'attachment'; the anchoring pass is a single linear walk that returns the same array reference when nothing changed.

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@lulumeya
lulumeya marked this pull request as ready for review August 13, 2026 23:27
@coderabbitai

coderabbitai Bot commented Aug 13, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Claude queued-command attachments now decode into queued user messages. Native Chat readers and the transcript watcher anchor queued prompts to transcript file order while preserving state across incremental reads, pagination, and snapshot replacement. The change adds unit tests for decoding and timestamp anchoring, live-tail and pagination tests, replay and pending-send regression coverage, and an end-to-end test for Native Chat display order.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.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
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly identifies the main change: displaying prompts queued by Claude while the agent is busy in Native Chat.
Description check ✅ Passed The description is detailed and covers the change, rationale, linked issue, visual proof, testing, AI disclosure, review considerations, limitations, and checklist. The Agent skill upstream boundary s…
Full details: Description check

Explanation

The description is detailed and covers the change, rationale, linked issue, visual proof, testing, AI disclosure, review considerations, limitations, and checklist. The Agent skill upstream boundary section and explicit Notes heading are not included, but the description is otherwise complete and directly relevant.

  • Fix all pre-merge checks with AI

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.

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

Actionable comments posted: 3


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 39f2e92a-bfe1-4583-bf36-fe5f2bce87d3

📥 Commits

Reviewing files that changed from the base of the PR and between e7b8526 and dbbebb8.

📒 Files selected for processing (10)
  • src/main/native-chat/queued-prompt-file-order.test.ts
  • src/main/native-chat/queued-prompt-file-order.ts
  • src/main/native-chat/transcript-line-decoders-claude.ts
  • src/main/native-chat/transcript-line-decoders.claude.test.ts
  • src/main/native-chat/transcript-reader.ts
  • src/main/native-chat/transcript-tail-reader.ts
  • src/main/native-chat/transcript-watch-engine.ts
  • src/main/native-chat/transcript-watch-queued-prompt.test.ts
  • src/shared/native-chat-types.ts
  • tests/e2e/native-chat-queued-prompt-visible.spec.ts

Comment thread src/main/native-chat/queued-prompt-file-order.ts
Comment thread src/main/native-chat/transcript-tail-reader.ts Outdated
Comment thread src/main/native-chat/transcript-watch-engine.ts Outdated

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

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 6b85ab4b-fe30-44d0-af87-f8d0e9403bd5

📥 Commits

Reviewing files that changed from the base of the PR and between dbbebb8 and ba1fa88.

📒 Files selected for processing (6)
  • src/main/native-chat/queued-prompt-anchor.ts
  • src/main/native-chat/queued-prompt-file-order.test.ts
  • src/main/native-chat/queued-prompt-file-order.ts
  • src/main/native-chat/transcript-tail-reader-queued-page.test.ts
  • src/main/native-chat/transcript-tail-reader.ts
  • src/main/native-chat/transcript-watch-engine.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/main/native-chat/queued-prompt-file-order.test.ts
  • src/main/native-chat/transcript-tail-reader.ts
  • src/main/native-chat/transcript-watch-engine.ts
  • src/main/native-chat/queued-prompt-file-order.ts

Comment thread src/main/native-chat/transcript-tail-reader-queued-page.test.ts Outdated
@grboy6770

Copy link
Copy Markdown
Contributor

I came at this record gap from a different direction — tracking down why a composer
bubble sometimes stays below turns that landed after it — and had started on a fix
and tests of my own before I found this PR. Yours got here first and covers more
than mine did, the file-order anchoring and the pagination path in particular, so I
have dropped my branch. What I do have is measurement, and a few notes I thought
were worth passing along.

Independent replay against this branch. I pulled a contiguous 17-record window
out of a production session (Claude Code 2.1.220) around one of these prompts and
redacted it: ids remapped, free text replaced with sentinels, all timestamps
shifted by a single constant, with record types and the queued_command fields
left verbatim. Running it through the production reader on
fix/native-chat-queued-prompt-visibility:

✓ lands between the turns that bracket it
✓ recovers it the same way when the records arrive while the pane is open
✓ ignores the notifications Claude queues for itself

The same replay on main drops the prompt entirely. The incremental case is the
one I was most curious about, since that is how it happens in practice, and the
anchoring holds up there.

How often it comes up. In that session, of 6 prompts typed while the agent was
working, 4 left no user record. They line up exactly with the queued_command
attachments carrying origin.kind: "human"; the other 2 were picked up after the
turn ended and got ordinary user rows. A second session on 2.1.234 looks the
same, so it does not seem version-bound.

On whether a queued prompt might not have been delivered. I wondered about
this and checked, so noting it here: interrupting a turn with a prompt still queued
writes queue-operation/dequeue and then an ordinary user record, with no
queued_command attachment at all. And every human attachment I found hangs off
the running turn's parentUuid. So the attachment only appears once the text has
actually been taken into a turn.

One small thing you may already have weighed. The filter here is
commandMode === 'prompt', which is exact on everything I looked at. Alongside it
there is a second field that separates the same two groups perfectly:
origin.kind === 'human' on the typed prompts against origin: null on all 99
task-notification entries, no crosses either way. Requiring both would just mean a
future version writing prompt mode from a non-human origin would not quietly
become a user turn. Entirely your call — the current check is correct today.

One more note in case it is worth mentioning in the PR body: this same gap is what
leaves the optimistic composer echo stranded. The echo is retired by matching a
user row's text, and optimistic rows render after the transcript regardless of
timestamp, so with no row to match it sits below later turns. That behaviour goes
away with this change too.

The redacted fixture and the replay test are yours if they are useful — happy to
open them as a PR against your branch, or to just drop them here. Equally fine if
you would rather keep the fixtures in one style.

@lulumeya

Copy link
Copy Markdown
Author

Thank you for going as far as replaying a redacted production window through the reader on this branch — that independent replay is the strongest external validation this PR has had, since everything else so far is my own local sampling. And retiring your own branch to consolidate here was generous; I appreciate it.

Your point 3 settles something I had been unsure about myself. I did not have a clean argument that a queued_command attachment implies the text was actually taken into a turn. The interrupted-turn path you traced — queue-operation/dequeue plus an ordinary user record with no attachment, and every human attachment hanging off the running turn's parentUuid — closes that question for me.

On origin.kind === 'human': I ran your cross-tabulation over a wider local sample, 151 transcripts and 1,932 queued_command attachments, and at that scale the two fields stop separating cleanly:

commandMode origin count
prompt kind: 'human' 1,035
task-notification field absent 889
prompt kind: 'peer' 6
prompt field absent 2

I inspected all 8 records the stricter check would drop. The two with no origin field at all are ordinary prompts a human typed, from June 2026 — that version simply did not write the field yet. The six peer records are messages delivered into the session by another agent session (they arrive as <agent-message from="...">); the TUI renders them as user turns, so chat should show them too. So the extra condition would reintroduce the bug for exactly the records this PR exists to keep, and the failure asymmetry runs the other way — a leaked notification is a stray bubble, a dropped prompt is the thing we are fixing. commandMode is also the field that has stayed stable across versions in my sample; origin is the newer arrival. I am keeping the check as is. To be clear, your split was real in your data — the crossovers only appear once older versions and peer-delivered messages enter the pool, so this is a sample-size difference, not an error on your part. It was the right instinct to check.

On the composer echo: that matches what the change does, but I have not traced that path myself yet, so I will not claim it in the PR body until I have. I will look.

And yes, I would like the fixture and replay test — drop them in a comment or open them against the branch, whichever is less work for you. Whether they land in this PR or a follow-up is not only my call, though: CI has not been approved to run yet (first-time-contributor gating), so the branch is currently verified locally and by your replay rather than by this repo's CI, and a maintainer has now been assigned. I would rather let them weigh in on scope.

@lulumeya

Copy link
Copy Markdown
Author

@grboy6770 — yes, please open it against this PR

In my last comment I said I'd defer on scope, but I've decided I want the fixture and replay test in this PR. Please open a PR against fix/native-chat-queued-prompt-visibility on my fork — that's easier to review than a pasted blob. If @brennanb2025 prefers it split into a follow-up, we can do that with nothing lost; until then, here is where it belongs.

@brennanb2025 — CI is blocked on a workflow approval

Every "PR Checks" run on this PR (four since 2026-08-13) is stuck at action_required — GitHub's first-time-contributor gating on fork workflows. Nothing on my side releases it; it needs someone with write access to click "Approve and run". Could you approve it when you get a chance?

Where the PR stands:

  • Branch is mergeable, no conflicts.
  • All review threads resolved (CodeRabbit raised four; all addressed).
  • Local gates pass: lint, typecheck, build, test suite.
  • @grboy6770 independently replayed a redacted production transcript window against this branch and confirmed the fix.
  • CI itself has never run, so none of the above is verified by this repo's own pipeline — which is exactly why the approval matters.

@grboy6770

grboy6770 commented Aug 19, 2026 •

Copy link
Copy Markdown
Contributor

The fixture and replay test are up against your branch — four cases, three of which
fail on c61f2963, no production code. I rebuilt it from a synthetic session after
the first attempt, so nothing in it is derived from a real transcript.

One correction to my earlier comment, since it is still sitting above and I would
rather it not be acted on. I suggested pairing commandMode === 'prompt' with
origin.kind === 'human'. @lulumeya showed that breaks down at scale, so I ran the
same cross-tabulation over my own wider sample — 370 transcripts, 1,382
queued_command attachments — and got the same answer:

commandMode origin count
task-notification field absent 1,042
prompt kind: 'human' 277
prompt field absent 63

Those 63 are ordinary typed prompts from a version that did not write the field
yet. The stricter check I proposed would drop all of them — 18% of the typed
prompts I have locally. commandMode alone is the right check. My original sample
was a single session.

@lulumeya

Copy link
Copy Markdown
Author

@grboy6770 You were right — I traced it, and the stranded echo is the same gap. Good catch.

The retirement path is prunePendingSends in native-chat-pending.ts, which leans on advancedNativeChatUserContentCounts (native-chat-pending-occurrence.ts). That counter tallies role: 'user' messages by normalized content key and only counts one as "advanced" once a non-user turn follows it; prunePendingSends keeps any echo whose occurrence exceeds the advanced count for its key. It doesn't filter on source or the queued flag, so a decoded queued turn counts like any other user turn.

Before this change, a prompt typed mid-turn produced no user record at all — its key never appeared, the advanced count stayed at zero, and the echo was kept indefinitely. And since pendingSendsAsMessages stamps the echo with the local send time, it sorts to the end of the list, which is why it sinks below later turns instead of holding position. With the decoder now emitting the queued prompt as a role: 'user' turn anchored between its bracketing turns, the next assistant turn advances it and the echo retires normally.

I pushed two characterization tests to native-chat-pending.test.ts — one keeping the echo when no user row lands, one retiring it once the queued turn is present. Removing the queued row from the second case fails it, so the pair isn't vacuous. Behaviour-only, no production change. I'll add this to the PR body as a second user-visible consequence.

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/native-chat/__fixtures__/queued-prompt-trace.ts (1)

1-9: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Condense the fixture header comment.

Keep the file-order rationale. Remove the repeated transcript-shape details.

As per coding guidelines, comments must be concise, non-obvious, and brief; prefer one line.

Source: Coding guidelines


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4051ec68-54da-48c1-91ef-93250a370720

📥 Commits

Reviewing files that changed from the base of the PR and between 2e6e0f6 and 5e8c363.

📒 Files selected for processing (2)
  • src/main/native-chat/__fixtures__/queued-prompt-trace.ts
  • src/main/native-chat/queued-prompt-trace-replay.test.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

Comment thread src/main/native-chat/queued-prompt-trace-replay.test.ts
@nwparker nwparker removed the P1 label Aug 19, 2026
@lulumeya
lulumeya force-pushed the fix/native-chat-queued-prompt-visibility branch from a7ec427 to c79dd50 Compare August 20, 2026 13:37
lulumeya and others added 6 commits August 31, 2026 09:22
A prompt sent mid-turn is queued by Claude Code, and the queue entry is almost
always the only record it ever gets — no `type: "user"` line is written for it.
`decodeClaudeTranscriptLine` returns null for every record that is not
`user`/`assistant`, so the prompt was missing from chat entirely: the terminal
showed it, the model answered it, and the transcript pane had no trace. The
renderer's optimistic echo then had nothing to match, so it pinned below the
newest reply until `PENDING_SEND_LIMIT` evicted it. Users read that as their
prompt vanishing.

Decode `attachment` records carrying a `queued_command` into an ordinary user
message. Content goes through `claudeContentBlocks`, the same path ordinary
turns take, so image-carrying queued prompts decode too. Restricted to
`commandMode: 'prompt'`: the other mode is the harness queueing
task-notification blobs for itself, and the orchestration transcript path has no
noise filter to drop those downstream.

Anchor the decoded prompt to the position the transcript gave it. Claude stamps
the record when the prompt was enqueued but appends it once the queue drains, so
sorting by time alone lifts it above turns that already happened. Placing it one
tick past its predecessor keeps chat in the order the terminal showed. The watch
engine carries that predecessor across reads because a live append batch can
open with the queued record itself, leaving nothing in-batch to anchor against.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three paths could still show a queued prompt out of transcript order.

Equal timestamps fell through unanchored, but the sorter breaks those ties on
id, which can lift the prompt above the record it was appended after. Anchor
those too.

The tail reader anchored after windowing, so a queued prompt opening a page kept
its enqueue stamp once its predecessor fell outside the slice. Anchor the whole
chronological read, then window the anchored array.

The watch engine seeded its carry-over timestamp only on the bounded snapshot
path; an unbounded initial drain emitted unanchored messages and left the seed
null, so a later queued-only append had nothing to anchor against either. That
carry-over now lives in a small anchor object, so every emit path shares one
way of tracking the predecessor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The seconds-only template produced `10:00:60` for the final turn, which is not a
valid ISO time — `Date.parse` returned NaN and that record decoded with a null
timestamp. The assertions happened to target the queued prompt, so the fixture
lied without failing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ands

The optimistic echo is retired by matching a transcript user turn's content, and
only once a later turn advances past it. A prompt typed mid-turn left no user row
at all, so the echo never matched — and it carries the send time, so it sat below
the turns that landed after it instead of in place.

Two cases record both halves: no user row keeps the echo forever, and the queued
user turn this branch decodes retires it. Behaviour only, no production change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A synthetic transcript window — the queue pair, the queued_command attachment
placed after the row it follows while carrying the earlier enqueue time, and the
assistant turns that bracket them — goes through the production reader. Covers
the cold read, the incremental append a live pane sees, and a negative for the
attachments the agent queues for itself.

Three of the four fail on the commit this branch started from.
The case read batches straight from `readIncrementalTranscriptMessages`, which
hands back physical file order, and asserted on the concatenation — so it passed
with anchoring removed. Routing the batches through the same anchor the watcher
holds across reads was not enough on its own either: the record the prompt has to
stay behind is a tool result, and the text assertion filters those out, so the
sorted order looked right regardless.

Assert the relationship instead. The prompt's enqueue stamp predates the record
it was appended after, and only the anchor moves it past. Disabling
`anchorQueuedPromptsToFileOrder` now fails this case and nothing else in the file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lulumeya
lulumeya force-pushed the fix/native-chat-queued-prompt-visibility branch from c79dd50 to 1710575 Compare August 31, 2026 00:23
@greptile-apps

greptile-apps Bot commented Aug 31, 2026

Copy link
Copy Markdown

Greptile Summary

The PR makes Claude prompts queued during an active turn visible in native chat and preserves their transcript-file ordering.

  • Decodes human queued_command prompt attachments as queued user messages.
  • Anchors queued-message timestamps to their physical transcript position across full reads, pagination, live appends, and replacements.
  • Adds unit, live-tail, renderer characterization, replay, and end-to-end coverage.

Confidence Score: 5/5

The PR appears safe to merge with no actionable correctness or security defects identified.

The queued attachment decoder is narrowly gated, and ordering state is consistently applied across full reads, pagination, live appends, and transcript replacement paths with focused regression coverage.

Important Files Changed

Filename Overview
src/main/native-chat/transcript-line-decoders-claude.ts Adds narrowly gated decoding of Claude queued prompt attachments into transcript-backed user messages.
src/main/native-chat/queued-prompt-file-order.ts Introduces an idempotent ordering pass that adjusts queued-message timestamps relative to their physical predecessors.
src/main/native-chat/queued-prompt-anchor.ts Carries the latest anchored timestamp across incremental transcript batches and supports snapshot adoption and reset.
src/main/native-chat/transcript-tail-reader.ts Anchors the complete chronological read before pagination so page-boundary prompts retain their predecessor context.
src/main/native-chat/transcript-watch-engine.ts Integrates stateful anchoring into initial snapshots, replacements, incremental batches, and transcript resets.
src/shared/native-chat-types.ts Adds an optional backward-compatible marker identifying queued transcript messages.
tests/e2e/native-chat-queued-prompt-visible.spec.ts Verifies a queued prompt renders exactly once and in transcript-file order through the native-chat UI path.

Sequence Diagram

sequenceDiagram
  participant U as User
  participant C as Claude
  participant F as Transcript
  participant R as Orca reader
  participant V as Native chat
  U->>C: Send prompt while busy
  C->>C: Queue prompt with enqueue timestamp
  C->>F: Append preceding assistant/tool records
  C->>F: Append queued_command attachment
  R->>F: Decode records in file order
  R->>R: Anchor queued prompt after predecessor
  R-->>V: Emit ordered user and assistant messages
Loading

Reviews (1): Last reviewed commit: "test(native-chat): make the replay's inc..." | Re-trigger Greptile

@lulumeya

Copy link
Copy Markdown
Author

@brennanb2025 The description is rewritten — it now opens with why this is still needed after #16117. The short version:

The settling evidence: the production reader on current main, run over this PR's synthetic fixture, renders ["THINKING_1","ASSISTANT_TEXT_1","","","THINKING_2","ASSISTANT_TEXT_2"] — the mid-turn prompt is simply absent.

Your #16117 measurement (1,049 of 1,132 human queued sends producing an attachment and no user row) showed better matching cannot help, and I agree — this PR does not try to match better, it makes the record exist. The two changes are complementary.

Rebased onto df14d1a29 (head 1710575bb, zero behind, MERGEABLE); the only conflict was import blocks in transcript-watch-engine.ts, no logic conflict. Typecheck, lint, build, the native-chat suites (1,628 tests), and the e2e all pass. On the full suite this branch introduces no failure — the three files that fail only here each pass in isolation, and main has one of its own — but neither run is green, so no clean-suite claim there.

CI has never run on this PR: every workflow sits at action_required under the first-time-contributor gate, which only write access can release — could you approve the run? A review when you have time would be welcome.

For scale: production code is +143/−6 across 7 files; the remaining +741 is tests and fixtures.

@lurunzi

lurunzi commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Additional decoder evidence from Claude Code 2.1.278: after queue-operation with reason: absorbed_mid_turn, a human message can appear as an attachment containing a queued_command with commandMode: prompt, humanTurn: true, origin.kind: human and the original text in prompt.

I added a sanitized cross-layer regression and a narrow decoder change in my fork. All 15 cases passed again on Windows today: decode the attachment, reconcile the pending echo, leave the turn lifecycle unchanged, and reject non-human, queue-only or unsupported records. No message was resent. The fixture replaces actual text and identifiers.

This only supplies decoding/delivery evidence; it does not fix timestamp-versus-file ordering. Your ordering work in this PR remains relevant. I am sharing this here rather than opening a duplicate PR. Codex assisted with the local fix and this verification.

@brennanb2025

Copy link
Copy Markdown
Contributor

Thanks for the detailed investigation and contribution! Merged #31204 now reads Claude’s queued prompts into chat history in the order Claude consumed them, including after a reload. Closing this PR as covered by that fix.

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.

[Bug]: Prompts sent while Claude is busy disappear from native chat, and reappear out of order

6 participants