mirror of
https://github.com/stablyai/orca.git
synced 2026-11-04 11:07:42 +00:00
3a18a7c67904320510126c4fef154d2347331ecd
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b922342199 |
feat(native-chat): the chat reads "Stopping…" from your Stop until the turn actually ends (#24369)
* test(native-chat): a Stop over a card sent now into the running turn keeps it paused
Red on main: Codex's turn end withdraws the steered hand-off and the queue
sends the card again as a new host turn, with no pause recorded.
* fix(native-chat): a Stop's queue pause holds a card whose hand-off is still unanswered
A card sent now into the running turn was still pending when Stop judged the
pause, so nothing was recorded; the interrupt then withdrew the hand-off, the
card went back to waiting unpaused, and the queue sent it again as a new turn.
The pause now counts a hand-off that may still return to waiting, judged with
the appended row applied, so a withdrawal lands under the pause and an
acceptance retires it in that same write. Codex and Claude both hit it.
* test(native-chat): the Claude re-send case fails on its diff, inside the test's budget
* test(native-chat): a pause held by an unanswered hand-off ends on every path that ends it
The provider's answer, the provider dying, the chat closing, a restart, a
withdrawal still owed at open, and a /clear (refused until the hand-off ends,
then carrying every waiting card paused 'cleared'); each ends with the queue
sending again.
* fix(native-chat): narrow the pause's settled hand-off, and assert the queued receipt's card
* refactor(native-chat): derive the queue's pause from Stop and Resume journal rows
Stop now appends one journal row where it takes effect, before the interrupt,
whatever the queue holds; Resume appends its own. The pause is a pure function
of the fold: the latest Stop with no later Resume and no later accepted turn a
person asked for. A /clear's carried cards name their source, which is the
replacement's 'cleared' pause. Host-origin turns never lift either.
One predicate decides which cards a pause holds; by default every waiting card
without a hold of its own, including one queued after the Stop. The drain's
consume re-judges it inside its own transaction.
The rows are tombstones of an id no item takes, carrying the mark: a released
host reads an unknown row kind as corruption and truncates the journal there.
Deletes the stored pause (recordPause, the retire hook on every appended row,
the settle-before-record step, mayReturnToWaiting and its row overlay) and the
tests that only proved it retires. The queued_message_pauses table stays in the
schema, unread and unwritten, for downgrade safety.
* fix(native-chat): a card queued after a Stop sends normally, never ahead of held ones
A Stop's pause now holds only the cards queued before its row, plus a steer it
withdrew, which returns to its own place. Each card records the journal
position it was queued at, and the one hold rule compares that with the Stop
row. A card queued after the Stop is a new instruction: it sends as usual, but
the drain still stops at the first held card, so it never overtakes them.
/clear's pause holds the cards it carried. Holding every card again is a
one-line switch in that rule.
* fix(native-chat): the queue's own send re-checks the no-overtake rule in its transaction
The drain's pick and its consume now read one function, nextSendableQueuedCard,
so a Stop row that lands between them holds a newer card behind an older held
one exactly as the pick would. Notes why Stop and Resume ride a tombstone row.
* fix(native-chat): stop creating the unused queue pause table
The queue's pause is derived from journal rows, so nothing reads or writes
queued_message_pauses. It was still created on every open "for downgrade
safety", but an older build creates it itself when it opens the database, so
the table only sat empty in every new database. The tests now pin that no
pause table exists.
* fix(native-chat): a Stop's pause never hides the restart pause
A Stop holds only the cards queued before it. The pause derivation still
returned the Stop alone whenever it was in force, so the restart pause was
never considered: a card queued after the Stop, written by a host process
that has since exited, sent by itself after Orca restarted, with no pause
header and no Resume. A /clear pause that held nothing could hide it the
same way.
Every pause in force is now derived. A card is held if any of them holds
it, and it names the first that does. The drain's pick, the consume
transaction's re-check and the published header all read that one rule;
the header names the pause holding the first card Resume would send.
* test(native-chat): pin the Stop's no-resend, lift and held-card rules
- The Claude and Codex Stop-withdraws-a-steer tests checked "not sent
again" at one instant, before a queue ignoring the pause re-sends. They
now wait for the stopped turn to end and re-check after a quiet window.
- The deleted-card test read a card queued after the Stop, which sends
whether or not a person's turn lifts it; it now reads the Stop's pause
before and after that turn.
- Unit cases pin that a Stop holds a card with no recorded position and one
queued before a rewind.
* refactor(native-chat): a Stop writes one Stop event with its reason, turn and caller
The Stop row that paused the queue becomes the general Stop event
{ reason, turnId?, at, caller? }, whose reason is the host's existing stop
cause. It still rides a tombstone of a host-only id (a released host deletes
the journal from the first unknown row kind), and Resume keeps its own marker
on its own id. Only a person's Stop (reason user-stop) pauses the queue.
* test(native-chat): a rewind keeps a lifted /clear pause lifted and restates the same Stop event
* test(native-chat): pin that Stop and Resume rows never reach apps or count as history
* test(native-chat): only a person's Stop event pauses the queue
* test(native-chat): pin that a Stop's event precedes the interrupt and the at-start stop
Through the real host: the event names the turn and who asked and is in the
journal when the interrupt reaches the agent; at an agent still starting it is
there before the start is ended and holds a card queued before it; an idle Stop
writes one only when it withdrew a send; and the queue's claim re-judges a
pause that landed after its pick.
* test(native-chat): a card held at a starting agent is checked before the Stop's timing
Also says precisely what the claim's in-transaction pause check defends
against: the Stop and the drain share one serialized lane.
* test(native-chat): a released build keeps and folds a journal holding Stop events
Replays this build's rows from the released build's own journal database: every
row is kept, the history after the Stop still folds, and an older client is sent
only removed ids no item uses.
* style(native-chat): format the Stop event changes
* test(native-chat): type the released build's exports through one checked helper
* fix(native-chat): the Stop/Resume row guard narrows to those tombstones only
* test(native-chat): run the Stop-event downgrade test in CI, and cover a writable downgrade
The Stop-event downgrade test ran in no CI lane: unit shards exclude the
cross-version folder, and the cross-version lane runs a fixed file list that
did not name it. It is now on that list.
Its only case replayed the rows into a release's own fresh database, because
that release cannot open the current host database. A second case opens the
journal this build wrote with a main build that shares the database: it opens
writable, keeps every row, appends, and this build then reopens it with the
person's Stop still pausing the queue.
* fix(native-chat): a Stop that stops nothing new writes no Stop event
A Stop reaching a running agent wrote a Stop event on every press. Two
presses before the first interrupt landed wrote two events, so a card
queued between them counted as before the latest Stop and was held,
though a card queued after a Stop should send normally. A Stop naming a
turn that had already ended, as a phone sends late, also wrote an event
for a turn it never stopped.
It now writes one only when it withdrew a queued send, or stops something
no event records yet: not a turn the journal no longer runs, and not the
live turn a Stop still in force already names, unless a card was handed
over into it since, which this Stop's interrupt sends back and must hold.
The interrupt and the "already finished" note are unchanged. A Stop at a
starting agent still always writes.
* test(native-chat): pin that a later host, eviction or close Stop never lifts a person's Stop
* chore(native-chat): put each Stop-row doc on its own declaration, and say only user-stop is journaled
* fix(native-chat): any later Stop event ends a person's Stop pause
A person's Stop paused the queue until their next accepted turn or Resume,
and a later Stop of another reason (the host stopping the agent, an
eviction, a close) was ignored. Now the pause is the latest Stop event's:
a later Stop of any reason ends a person's pause, and only a person's Stop
pauses. The fold keeps the latest Stop event whatever its reason.
An eviction of a resting chat writes no Stop event (a Stop that stops
nothing writes nothing), so it cannot release held cards; a test pins that
no event means no lift.
* fix(native-chat): a second Stop press is a repeat even when the first came before the turn showed
A Stop pressed before the agent's turn shows in the journal (before
Claude's echo, or before Codex opens the turn) records no turn. A second
press once the turn showed compared that missing turn with the live one,
wrote a second Stop event, and held a card queued between the presses.
A repeat is now judged by what was sent since the Stop in force: with
nothing sent after it (a refused send aside), a Stop that named no turn,
or named the live one, is repeated and writes nothing. Anything sent since
and not refused, including a send whose fate is unknown, makes the new
press write, since its interrupt may send that card back to waiting.
Tests: the two-press case across the turn showing; a steer between the
presses settled unknown; and a Stop naming a turn that ended while the next
card is sent but shows no turn yet, which writes and holds that card. The
fold test that claimed an eviction path is renamed.
* fix(native-chat): the queue's pause ignores a Stop or Resume row holding a value no build writes
A Stop or Resume row's value is read from disk with no shape check, and
the pause fold stored whatever it found. A stored `stopEvent: null` would
then throw on every pause check for that chat: the queue's pick, its
send, and every queue update to clients. No build writes such a row, so
this is hardening.
The fold now reads a Stop only when it is an object with a string reason
and a finite time, and a Resume only when it is `true`. Anything else is
ignored: it pauses nothing and ends nothing. The row is still not treated
as malformed, which could cut the history short.
* fix(native-chat): a Stop still reads as yours after Orca restarts before the turn ends
Every stop that ends work now writes the Stop's event before it ends the child: a
person's close of the chat, an eviction (worktree teardown, orchestration stop, tab
cleanup) and the idle sweep's stop of a start that never landed. A stop that ends
nothing writes nothing, and quit writes none: its resume marker records why.
The turn-end write reads the latest Stop event where every turn row is built, so the
adapter's settle, the host's fallback and the relaunch's settle all agree: a turn a
person's Stop or close named, ending with no verdict of its own after that Stop, ends
as their cancellation. A relaunch's probe-bounded end is no earlier than a Stop that
found the turn running. When the provider refuses the interrupt and the turn runs on,
a refusal row answers the Stop, so a later crash still reads Failed; pressing Stop
again after a refusal is a new Stop.
* refactor(native-chat): a stop no longer carries its cause; the turn's end reads the Stop event
The cause of a stop was threaded in memory from each entry through the host's stop
step, the adapter router and each adapter's close onto the `ended` it settled with,
and Claude kept a per-turn copy of a Stop it sent. All of that is gone: adapters
settle a turn they cut as interrupted with no verdict, the host's fallback does the
same, and the one rule where a turn row is built (`turnEndAfterStop`) reads the
journal's latest Stop event to say whether it was a person's.
- `closeSession` / `disposeSession` take no cause; `ended` has no `stopCause`.
- Claude reads an error result after a person's Stop as their cancellation from the
journal's Stop event (through the event sink), not from a per-turn slot, and a
refused interrupt is the host's refusal row, not `withdrawTurnStop`.
- An owed wind-down keeps no cause: its retry's fallback reads the Stop event.
- The mutation context's Stop passes no cause: its step already wrote the event, and
the delivery loop's child-end reason is read back from it.
- A Stop pressed before its turn showed applies to the turn that opens under it,
unless a send a person made since was accepted.
* test(native-chat): a turn a later send opened is no Stop's that named no turn
* test(native-chat): the restart test's death proof carries its detail
* refactor(native-chat): a refused Stop leaves no record; a Stop only ever ends the turn it names
The stop-refused mark is gone: its tombstone kind, its fold, the clock-keyed match that tied it to
a Stop, and the exception that let a second press after a refusal write a new Stop. A Stop that
stops nothing writes nothing. A Codex refusal names a turn that is no longer its active one, and
the Stop names that turn, so the turn running instead never reads as the person's by its id alone.
* fix(native-chat): a Stop pressed before any turn showed stops only the turn opened next
A Stop that named no turn read as the person's cancellation for every later turn that opened
after it, until a send a person made was accepted. The queue's drain, orchestration mail and a
restart continuation send as the host, so a turn they opened long after, cut by a crash, read
"Interrupted" as if the person had stopped it. The Stop now applies only to the first turn
opened after it.
* fix(native-chat): an older Claude's error end after a Stop pressed before its echo reads Interrupted
Claude CLIs before 2.1.91 end an interrupted turn with an error result that names no reason. The
translator judged whether a person's Stop explained it by its own copy of the Stop rule, which
ignored a Stop that named no turn, so a Stop pressed before Claude echoed the send read "Failed".
The translator now writes such an end as interrupted with no verdict and no error row whenever a
person's Stop may name the turn, and the journal's one rule decides as it writes the end.
* fix(native-chat): a person's Stop and /clear each name why they end the agent
The host's mutation path ended the agent with one "recorded" ending for every caller, which read
back the reason of whatever Stop event the journal held last, however old. /clear writes no Stop
event, so its end took an unrelated earlier reason. Each caller now names its own: the chat's Stop
`user-stop`, whose event its own step wrote, and /clear `user-close`, the user replacing this chat.
* fix(native-chat): a host stop judges whether it ends work after the provider's rows land
A close, eviction or host stop decided whether it ended a running turn from the journal as it
stood, while the provider's own rows (the turn its echo opened) could still be in the session's
event sink. A close landing in that gap wrote no Stop event, so the turn it cut read as news. It
now reads after the sink drains, as a person's Stop does, through the same check; a drain that
fails or takes over a second reads working.
* fix(native-chat): a Claude Stop naming a turn that just ended still marks the follow-up it cuts
A phone names the turn it last saw. When that turn had ended and a follow-up was still unechoed,
Claude's Stop interrupted the follow-up and ended the child, but the Stop's event named the ended
turn, so the follow-up's turn the child's end cut read "Failed" under "Cancellation requested.".
A Stop that ends the provider's session ends whatever is in flight, so its event now names the
live turn or none, and a Stop that names none binds the turn opened next. Codex keeps naming only
the turn the Stop names.
The Claude Stop turn-end tests move to their own file, since the session-ending Stop suite is at
its line budget.
* fix(native-chat): the idle sweep reads working by the same rule as a stop's event
The sweep judged a chat resting while a send whose reply was lost was still unanswered, but the
stop's event writer counts that send as work. So the sweep evicted it and wrote an evict event,
which ends a person's Stop pause and let the cards behind it drain on their own. The sweep's owed
work now reads the main agent working the way every session list and the event writer do.
* test(native-chat): an aborted eviction's injected drain failure lands on the eviction's own drain
A host stop now drains the session's sink once to judge whether it ends work, so the tests that
fail the eviction's drain-published step skip that first drain.
* fix(native-chat): the idle sweep's rest writes no Stop event; it evicts a send that never echoes
The previous commit made the sweep count an unanswered send as owed work, which pins a chat whose
admitted send Codex never echoes forever, and the sweep exists to retire exactly that. That rule
returns. The sweep stops only an agent it judged resting, so its eviction now writes no Stop
event, whatever send it retires: a person's Stop pause holds through it.
* fix(native-chat): stopping a start that carries no send writes no Stop event
A host stop, eviction or close of a starting child wrote a Stop event whatever the start carried.
A start with a send already reads working, so the clause only mattered for a start with none,
which ends no turn and no send: its event only lifted a person's Stop pause and bumped the idle
clock, which is why the idle sweep had been changed to close the conversation in the same pass.
The clause goes and the sweep is #24072's again. The child's end still reads host-stop, as before.
* test(native-chat): a Stop's pause across a restart is tested with a restart that writes no event
The rig's restart closes the chat with an eviction, which now writes a Stop event when work runs
and so ends a person's Stop pause. "A Stop never hides a restart's pause" then passed with no Stop
pause left to hide anything. Those tests, and the pause-lift test whose dropped assertion returns,
restart as a process that dies with no close, which like a quit writes no Stop event, and assert
that both the Stop's and the restart's pauses are in force first.
* fix(native-chat): a host stop of a turn a person's Stop is still ending keeps that Stop's reason
An eviction or host stop that landed while a person's Stop or close was already ending the same
turn wrote a newer Stop event, and the turn's end reads only the latest, so the person's Stop of
that turn read as news. A host reason now writes nothing while a person's Stop still decides what
runs: the live turn it names or bound, or, with none, the turn a send opens next. The person's
own close still writes. The E2 tests now open and end the stopped send's own turn, as Codex does,
so the mail turn after it is not the turnless Stop's.
* fix(native-chat): an older Claude's error on a later turn keeps its error text after a Stop
The translator left an error result that names no reason to the journal's Stop rule whenever a
person's Stop named the turn or none, but the rule binds a Stop naming no turn only to the turn
opened next. So a real error on a later turn read "Failed" with its error text dropped. The
translator now asks the journal's rule itself (`personStopDecidesTurn`, the one core
`turnEndAfterStop` and a host stop's in-force check share), so the two cannot disagree.
* fix(native-chat): a Stop of a start that never landed binds no later turn, whatever sent it
A person's Stop pressed while the agent starts names no turn, and the send it stopped is
cancelled before it opens one. The Stop then bound the next turn anything opened (orchestration
mail, a restart continuation, the queue's drain, all of which send as the host), so a host
eviction of that turn wrote nothing and its crash or close read as the person's cancellation. A
Stop that named no turn now binds only a turn no send journaled after it opened: any send since,
of any origin and not refused, opens its own. The E2 test's mail send is accepted as Codex
accepts it, instead of opening the stopped send's own turn first.
* test(native-chat): a rewind's restated turnless Stop binds no turn opened after the rewind
A Codex rewind restates a person's Stop still in force after the turns it keeps, at a new
sequence, so by sequence alone it would bind the next turn opened after the rewind. A send
journaled after the restated row voids that binding (the previous commit), which this pins.
* fix(native-chat): a relaunch settles a person's stopped turn with no "stopped while in progress" row
After a restart, a turn a person's Stop ended reads "Interrupted after N" with the muted mark, but
the relaunch still added the error row saying the provider stopped mid-response, which a live Stop
never writes. The settle now skips that row when every turn it interrupts is the person's Stop's
by the journal's one rule; a crash nobody stopped keeps it.
* test(native-chat): the unexpected-exit settle's journal fake answers whether a person's Stop decides a turn
* fix(native-chat): a host stop whose sink drain fails reads the journal as it stands
A host stop drains the session's sink before judging whether it ends work, and a failed or slow
drain read as working. So an eviction of an agent at rest wrote a Stop event that ended nothing,
which lifts a person's Stop pause, and a close wrote a person's event naming no turn. The drain is
now best effort: the stop goes ahead either way and only its record is at stake, so a failed or
slow drain leaves the journal's read as it stands. A person's Stop keeps its own rule.
* fix(native-chat): a Stop that named no turn applies only to a turn a send it stopped opened
A person's Stop pressed before any turn showed names no turn. It bound the first turn opened
after it, then (
|
||
|
|
7faa9f7cd3 |
fix(native-chat): reasoning rows with a real open/finished state, and readable Claude thinking (#19221)
* fix(native-chat): render structured reasoning as collapsible messages
* fix(native-chat): align expanded reasoning with summary
* fix(native-chat): place reasoning chevron after summary
* De-emphasize reasoning headlines with observed timing labels
* Exclude later turn work from observed reasoning duration
* fix(native-chat): give reasoning rows a host-owned open/closed lifecycle
A reasoning row now says whether its block is still streaming (`state`)
and, when the host saw it end, when (`completedAt`). The row's own
start is its first-write time, so "Thought for N s" is measured on the
execution host instead of by whichever window happened to be watching.
Rows open with their first non-empty text and close on every path that
ends them: the block's final frame, a new message in the same stream,
every Claude turn end through one hook on the open turn, Codex
item/completed, turn and session settlement, active-item eviction, and
both host sweeps for a dead generation. Closing writes are lifecycle
writes so backpressure cannot leave a row open. Rows without the field
(older hosts, older journals) never read as live.
The reasoning row's headline reads the row's lifecycle and its own
turn's liveness: Thinking while open in a running turn, then Thought
for N s, Thought when no span was seen, Reasoning when the host kept no
lifecycle.
* feat(native-chat): ask Claude for readable thinking summaries
Under Orca's launch the Claude CLI streams thinking blocks with empty
text, so no reasoning row ever had anything to show. Pass only
`--thinking-display summarized`: it fills thinking blocks with the
API's summaries without turning thinking on, so a user who disabled
thinking keeps it off.
Orca runs the user's own binary, and a CLI older than the flag exits on
it before the session starts. The launch probes the binary it is about
to run, overlapping the rest of launch resolution, and passes the flag
only when that probe has already answered with 2.1.94 or newer. A slow
or failed probe never delays a launch and is never remembered; a
successful one is kept per binary until the binary changes.
* test(native-chat): type the superseding send in the reasoning lifecycle test
* fix(native-chat): stop reading an ended reasoning row as thinking
The spinner line infers "Thinking" from the newest root row being a
reasoning message. With summaries on, a closed reasoning row stays the
newest row while Claude streams a tool's input, so the line read
Thinking for the whole Write. A reasoning row now counts only while its
own state is running; a row from a host that keeps no state reads as
before.
* fix(native-chat): measure reasoning from the block's start, not its first text
A row opens with its first summary text, which trails the thinking
block's start by seconds, and the journal stamps a row with its queued
append time. So "Thought for N s" read 1 s for blocks that ran 4.65 s
and 12.95 s. The block registry now records the host time at each
block's content_block_start (else its first delta), and every write of
that block's row carries it as the row's observed time; the close keeps
the frame's own receipt time. Codex reasoning rows likewise carry their
item/started time.
* perf(native-chat): stop rewriting the reasoning row for every thinking token
Claude sends a thinking_tokens frame after every thinking delta. Every
frame the stream path did not consume forced the streamed text out
first, bypassing the checkpoint widening and the coalescing window, so
a single thinking block was rewritten and republished once per token:
212 full-row writes and 258 KB for one captured block. A frame that can
write no row (token tallies, stream deltas no registry carries, pings)
no longer forces that flush; every frame that can write still does.
The same block now takes 14 writes.
* fix(native-chat): time Codex reasoning by receipt and close evicted rows
Codex summary text lands 13-50 ms before item/completed, inside one
coalescing window, so a reasoning row's first write can be its
completion. Item boundaries are now stamped with their host receipt
time the way turn boundaries already were, so a retried or buffered
delivery keeps it, and the item translator falls back to the host
clock rather than Date.now(). The row carries its item/started time on
every first write, including the completion, so its span is
item/started to item/completed.
An evicted active item is now closed from the text streamed so far,
like both settle paths, and the eviction runs before the incoming item
is tracked: tracking first let the stream bound drop the evictee's text
before the eviction could close its row, stranding it running.
completedAt now has one meaning everywhere: the host time the message
was seen to end, or the end of the turn or stream that cut it off;
absent only when no end was seen live.
* refactor(native-chat): keep the Codex streaming body translation pure
The streaming translation preserves a reasoning body as-is again; the
stream writer, which is what knows the item has not completed, stamps
it running.
* test(native-chat): keep a re-collapsed reasoning row collapsed through a revision
* fix(native-chat): estimate a collapsed reasoning row as its trigger
A reasoning row renders collapsed, as one small button, but its height
was estimated from its full text: a 4,129-character summary reserved
about 950 px for a 24 px row, so long chats jumped as rows were
measured. It is now estimated at the trigger's height; opening the row
remeasures it.
* fix(native-chat): probe the CLI the launch will run, and learn from a refusal
The thinking-display gate probed `claude --version` with Orca's own cwd
and env, while the launch spawns with the workspace's cwd and the shell
env. Behind a version manager's shim those can pick different CLIs, so
the probe could approve a CLI the launch never ran, and an older CLI
exits on the unknown flag before the session starts. The probe also
only counted if it had already finished when resolution did, so a
first launch, or the first after a CLI update, usually went without
the flag.
The probe now runs with the launch's own resolved cwd and env, is keyed
by the binary and the workspace, and a launch waits up to 200 ms for it
(about 3x the probe's measured p95) before going without the flag. Only
answers are kept, so a slow or failed probe is asked again next launch.
A child that exits with commander's "unknown option '--thinking-display'"
marks that binary in that workspace so the next launch skips the flag;
that one start fails exactly as any CLI startup failure does today.
* test(native-chat): type the thinking-display probe mock with both of its parameters
* fix(native-chat): recheck the account switch after the probe, last as before
Moving the invocation ahead of the probe, the transcript check and the
permission mode put its account-switch recheck before those awaits, so a
switch that began during them launched unchecked. The invocation is the
last await again. The probe gets its own env from the same sources the
launch uses, the inherited env and the overlay with the CLI's runtime on
PATH, built by the same code, minus every credential: asking a CLI its
version needs none.
* fix(native-chat): wait up to 1.5 s for a cold probe, and remember every outcome
200 ms only covered a warm CLI; a cold disk, a node install or an
antivirus scan exceeds it, and that chat's child then ran its whole
life without summaries. A launch now waits up to 1.5 s, once per binary
per workspace, measured from when that binary's probe began, so a later
launch never waits again on a probe already past it. The probe gets its
own 10 s kill timeout, and every outcome, including no version printed,
a failure or a kill, is kept for the binary's life, so a probe that
hangs costs one launch rather than every one. A refusal seen while a
probe still runs wins over its late answer.
* fix(native-chat): leave Thinking to the activity line while reasoning runs
A reasoning row still being written drew a pulsing "Thinking…" header
right under the turn's activity line, which already says Thinking: two
live indicators for one fact. In a running turn an open reasoning row
now draws nothing and reserves no height; it appears when it closes, as
"Thought for N s". A row from a host that keeps no state, a closed row,
and a row left open by a turn that ended draw as before. The row's
Thinking headline is gone with its catalog key.
* fix(native-chat): end every unfinished Codex item through one rule
A reasoning completion with no text of its own left the row its stream
wrote running for good: the completion translated to nothing and the
item left the active set, so no settle could reach it. It now closes
from the text streamed so far.
Settlement, eviction and that completion now build an unfinished item's
row through one choice (the streamed text when there is any, else the
item as it started) and end it through one rule. An evicted file change
with streamed tool output no longer keeps that output as its patch; it
reads as interrupted, as a settled one does. A completion whose start
was never recorded claims no span, so it reads "Thought".
* fix(native-chat): catalogue Claude's stream keep-alive as benign
An uncatalogued `ping` stream frame classified as substantive, so the
fallback wrote a visible "claude · message:stream_event:ping" row, and
since such frames no longer force streamed text out first, a ping
inside a coalescing window landed above the open reasoning row. A ping
is now benign: it writes no row.
* fix(native-chat): bound finding the binary by the probe budget, and keep it LRU
Resolving the command's real path and its mtime was awaited before the
budgeted wait, so a slow filesystem could hold a launch indefinitely;
it now counts against the same budget, and running out caches nothing.
The cache is least recently used rather than first written, and holds
32 binary-and-workspace entries rather than 16.
* feat(mobile): collapse reasoning rows the way desktop does
With summaries on, every Claude turn now carries reasoning text, and the
phone drew all of it inline, dimmed, between the prompt and the answer.
Mobile now draws a reasoning row as desktop does: collapsed to "Thought
for N s", "Thought" or "Reasoning", its text mounted only once opened,
and nothing at all while the row is still being written in the live
turn or has no text. The headline and the visibility rule live in one
shared module both clients read, so they cannot drift.
* fix(native-chat): let a failed CLI probe heal instead of latching
A probe killed at its timeout, failing to spawn under a loaded boot, or
printing no version was cached as "no flag" for the binary's life, so
that workspace never got summaries again in that run. Only a version
(either side of the floor) or the CLI's own refusal is kept for good
now; a probe that gave no version is kept for 10 minutes, so a hung CLI
still costs one wait per stretch and a boot-time failure heals.
* docs(native-chat): say exactly what the version probe's env leaves out
* fix(native-chat): record a Codex item's start whatever its first frame carried
The start was recorded only for an item/started that wrote no row, so a
reasoning item that started with text lost it and its completion
claimed no span. Every tracked item/started now records its receipt
time, and the started write carries it too.
* fix(mobile): label the reasoning toggle and give it a full touch target
The toggle now tells a screen reader what it is, "Reasoning: Thought
for 3s", as desktop's prefix does, and reaches a 44 pt target. The
shared English copy stays private to the module that formats it.
* refactor(native-chat): build reasoning rows from one provider-neutral helper
Claude, Codex and the terminal sweeps each built the reasoning row body and
its running/ended stamp themselves. They now share journal-reasoning-row:
blank text journals no row, text is bounded the same way, and an end carries
completedAt only when the host saw it.
* refactor(codex): move the active journal item type into the contracts file
codex-unfinished-item-body imported the type from the settlement module,
which imports values from it.
* test(claude): read the launch PATH the way Windows spells it
* test(claude): compare the probe's PATH to the launch's without Orca's CLI dir
When the CLI's directory also holds node (Linux CI's /usr/local/bin), the
runtime pairing puts that directory first, ahead of the Orca CLI directory the
launch adds, so the launch PATH no longer ends with the probe's. Both still
resolve the same claude and shims. The test now checks that exactly, for a CLI
with and without a sibling node.
* feat(native-chat): lead the reasoning row with a brain glyph in the tool-row column
* fix(native-chat): route the reasoning glyph through the shared icon names, keep its chevron findable, and match it on mobile
* fix(mobile): keep the long-press actions sheet on reasoning rows for Android
* fix(native-chat): forward every exit argument through the thinking-display connection wrapper
* refactor(codex): keep the receipt-timed notification methods with the event they stamp
* feat(native-chat): read an open reasoning block through the one live "Thinking" line
While the agent's open reasoning block has text, the turn's live activity line is its
disclosure: collapsed by default, expandable to the live text (capped and scrollable), and
the block's row draws nothing meanwhile. When the block ends, its row appears in place,
open if the reader opened it live, because the line and the row read one disclosure key.
Which block the line discloses is derived from the line's own render condition, so a row
is never hidden while nothing on screen shows it; any other open block (a subagent's, or
one a prompt pushed off the line) draws as "Reasoning". Desktop and mobile alike; no host
or wire change.
* fix(native-chat): a slot kept for its turn bar or diff rollup no longer draws its message
The transcript row drew the message of every message slot, while the slot builder pushes a slot
for a row it declined to draw whenever that row also carries its turn's bar or diff rollup. So the
open reasoning block the live line discloses still drew as a "Reasoning" row when it was a
provider-opened turn's first row or the last row of a turn that changed files, and one click
opened both. The builder's decision now travels on the slot (`drawsMessage`) and the row draws
only the bar and rollup when it is false; the row-level `folded` guard it made redundant is gone.
* refactor(native-chat): draw the live line from one shared value, with one live region
Desktop and mobile now render the live activity line from one pure function,
`nativeChatLiveLine`: whether it draws, what it says, and the open reasoning block it
discloses with the text it has so far. The open block's row is hidden from that same value,
so desktop no longer restates the line's render condition beside it, and the lines no longer
re-derive the text.
The line keeps one element, and so one live region, through every state; only its trigger
and body come and go, so a screen reader hears "Thinking" and the label after it. On mobile
the live text gets the finished row's Android long press (copy or select through the message
actions sheet), the line's touch target is the row's 44 pt, and its label and body sit in
the finished row's column so nothing moves when the row takes over.
* fix(mobile): keep the live text's actions sheet on the block it was opened for
On Android the sheet opened by a long press on the live reasoning was a flag gated on a live
block: it vanished when the block ended, mid Select text, and the stale flag reopened it
unprompted on the next block. The sheet now holds the message it was opened for.
* test(native-chat): the reasoning body owns its tone, live and once landed
Rendered QA on a pre-merge build showed the live line's open reasoning in full foreground and the
landed row's in muted text, so it dimmed as the block landed: the body set no colour of its own and
inherited one from wherever it was mounted. Since the main merge (
|