Repository navigation
fix(native-chat): show Thinking only while a reasoning block is open - #24545
brennanb2025 wants to merge 21 commits into
Conversation
The live turn's activity gains an optional reasoning field: whether the session's own agent, and which subagents, have a reasoning block or item open right now. Each provider derives it from the one tracker that also stamps the reasoning rows' running/completed state, so the signal and the rows cannot drift. Claude opens a block at content_block_start, blank blocks included, and closes it at its final frame, its content_block_stop, a new message in its stream, or the turn's end. Codex reads its live item set. A Codex item's activity copy now belongs to the item that set it: that item's completion clears it, and any other item's completion leaves it. This ends 'Thinking through the request' and 'Running a command' lingering after their item finished. Text and reasoning compose in one per-turn channel, so neither writer clears the other. A new shared gate, nativeChatReasoningOpen, is the one place clients read the signal.
…ever a row The live turn line said Thinking whenever the newest root journal row was a reasoning row. That row is history, not the present: nothing said Thinking before a summary's first text or when summaries are off, and a finished row kept saying it until another row landed. Desktop and mobile now read one gate, nativeChatReasoningOpen, for the turn line, for hiding a reasoning row while it is still being written, and for a subagent's section head and roster entry, which show Thinking while that subagent reasons. The newest-row inference is deleted. A host that sends no signal shows no reasoning-derived Thinking.
… what an old client reads
…ponent file Also assert the Codex signal is open before each close the tests pin.
Clearing the line on whichever item set it was too broad: a subagent spawn's item completes at once while the subagent keeps working, so 'Coordinating with another agent' vanished early. A reasoning item ending does mean the reasoning ended, so only that clears its own words; every other completion leaves the line as it was.
…en reasoning A caught-up client gets each activity as an empty batch at its cursor, and the reducer dropped one whose turn and words matched what it held. A frame that only opened or closed reasoning was therefore lost on desktop and mobile. The host's channel and the reducer now share one equality, which compares the reasoning too. Codex also published a reasoning item's end as two frames, words cleared and then reasoning closed, so Thinking flashed between them. One Codex event's activity updates now publish once, as their net result.
…gate - A running reasoning row that draws (an older host, a dropped frame) reads 'Reasoning', not past-tense 'Thought'. - The gate is keyed on what it answers, so activity text changes no longer rebuild the transcript. - A subagent's 'Thinking' uses the type its entry's state text already uses. - A subagent's unfinished reasoning is hidden only in the live turn, as the session's own is.
setActivity can be refused like any ordinary write when the sink's queue is full and holds no earlier activity to replace. The channel recorded the value as sent anyway, so a refused open or close was never sent again while it stayed current. setActivity now returns the queue's admission, and a refused value goes again on the next derive. The shared equality is renamed agentSessionTurnActivityEqual.
…s it works A Codex helper turn that never completes leaves its reasoning open on the host until the session ends, so the heading would keep saying Thinking across later turns.
Claude CLI 2.1.280 streams no subagent frames; a subagent's thinking arrives only finished. The streamed-subagent test is replaced by one built from that captured frame. A Codex helper thread's reasoning is pinned from a real 0.159 capture: the child's agent over exactly its span, never the parent's.
…SetActivity Giving setActivity a return type broke every sink mock that returns what it pushed. The admission now comes from trySetActivity, as tryAppendItem and tryPublish do beside their void forms, and the channel uses it when the sink has it.
…en reasoning Both joined ids with a newline, so ['a\nb'] read the same as ['a', 'b']. The equality now compares ids one by one, and the gate key is JSON of the session flag and the sorted ids.
…of latching a re-send A refused activity was re-sent only on the next provider event, and a refused turn-end clear never; Codex can go seconds without one. The activity op coalesces, so at most one small one is ever queued: it is now admitted past the ordinary watermark. It still counts toward pausing reading, never declares backpressure itself, and is refused once the queue is closed or failed. The re-send latch and trySetActivity are gone; setActivity stays void. Three Codex hard-watermark tests counted the clear as dropped; their counts move by the one activity op and its publish.
…he gate shut Real translator, sink queue, journal, subscribers and reducer: a block cut off by an error_during_execution result closes in the same delivery that clears the signal, because an activity publish carries every row journaled before it.
A subagent shows Thinking while the host reports its reasoning open and its roster entry says it works; its unfinished reasoning row hides exactly then, whichever turn its section sits in. This replaces the row hide's live-turn guard. On a roster entry, Thinking takes the place of the state word instead of standing beside it.
…reasoning-lifecycle
…d Gate only - The gate key tolerates a reasoning field with no subagent list, as the activity equality already does. - One exported NATIVE_CHAT_NOTHING_REASONING_OPEN replaces three local copies and the module's private one. - nativeChatReasoningOpen, used only by tests, is gone; they read the gate the way clients do.
…reasoning-lifecycle
…ts and stops reasoning
…reasoning-lifecycle
|
Status (step 2 of 2): reviewed and QA'd; stays draft until #19221 merges, then retarget to main and mark ready. |
ELI5
The chat's activity line says "Thinking" when the agent is thinking. Until now Orca guessed that from history: if the newest row in the transcript was reasoning, it said "Thinking". That guess was wrong in both directions. "Thinking" showed up late, only once reasoning text had been written. It then stuck around after the thinking ended, for example while Claude wrote out a large file. For Codex, whose reasoning arrives without text on Orca's settings, "Thinking through the request" stayed on screen after the reasoning had finished.
This PR is step 2 of 2, stacked on #19221 (step 1). The host now tells every client, live, whether a reasoning block is open right now. "Thinking" shows exactly while one is open in the running turn, and at no other time.
What Changed
What you see (before = today's release → after both steps):
How it works:
reasoningfield:{ session, subagents }.sessionsays whether the session's own agent has a reasoning block open, andsubagentslists the subagents that do. It is never written to the journal.item/startedand closes atitem/completed, or at the end of the turn or session.src/shared/native-chat-reasoning-row.ts(the signal says open, and this is the live turn). It drives the activity line, the subagent section headings, and step 1's rule that hides a reasoning row while it is still open. The old "newest row is reasoning" guess is deleted.Why
The root cause was that "Thinking" was inferred from history (the newest journal row) instead of read from what the agent is doing now. That is the same bug class #24218 fixed for the activity line's tool text. The common pattern is to show "Thinking" only while a reasoning lifecycle is open in an active turn; this PR follows it, with the host owning the lifecycle. Differences from the common pattern, each labelled:
Mixed versions:
Limits: Claude Code (2.1.280) never streams a subagent's thinking (recorded: no subagent stream frames in any run; in Orca's launch setup the subagent's thinking is not sent at all, only its start and finish). So a Claude subagent's section heading does not say "Thinking". A Codex subagent can only show "Thinking" while the parent's turn is running.
Linked Issue
Maintainer change; no issue. Stacked on #19221.
Visual Proof
Real recordings replayed at 1x into hidden test builds of step 1 (#19221) and of this PR on a separate Mac. A stand-in
claude/ Codex app-server replayed real Claude 2.1.280 and Codex 0.159 sessions; no real CLI or credentials.Codex, 2 s after a reasoning item completed (a 2 s pause was added after each reasoning item so a lingering label is visible)
Measured over the whole turn (250 ms sampler):
Testing
AI Disclosure
Author: @BrennanKB5
Review
Two rounds of independent review (host tracker and signal, clients and wire, design challenge, data path, UI and outcomes) plus a release-readiness pass (SSH/remote, crash/growth, security, performance, correctness, backward compatibility, cross-platform, organization). Round 1 found that clients dropped updates that changed only the reasoning signal; fixed, with a test through the real host-to-client path. Nothing P0–P2 is open. Deferred (P3): a Claude API retry keeps "Thinking" through the retry wait; a Codex subagent can only show "Thinking" while the parent's turn runs.
Agent skill upstream boundary
Notes
No dependency changes. SSH/remote: the signal is published by the host that runs the agent, and older clients ignore it. Performance: the signal is published only when it changes.
Checklist