Files
orca/src/shared
Brennan Benson 4634d2c03b fix(native-chat): let the provider reopen a Claude turn it resumed itself (#20518)
* fix(native-chat): let the provider reopen a Claude turn it resumed itself

A Claude turn could only be opened by Orca's own send echo, while any
`result` frame closed it. The provider resumes work on its own — a
background task reports in and wakes the agent after `result` settled the
turn — and nothing Orca sent ever arrives to reopen one, so the session
projected `idle` for the rest of the work. The model's own output is the
evidence a turn is running, the way Codex's `turn/start` is, so it opens
one; whichever opened it, the next `result` settles it.

Subagent frames still open nothing: children outlive the turn that spawned
them, and their work is their parent turn's, never a turn of its own.

* fix(native-chat): bracket a resumed turn around the output that opened it

A resumed turn was published after the frame's own rows, so its first tool
call sat above the turn record. Every reader that scans back to the turn
record and stops — the active-tool reader behind the sidebar's tool line,
and the turn-window activity selector — looked straight past it, and the
row showed working with no tool until a second call landed.

The turn now opens before its frame is journaled. A send's turn keeps its
existing order: the user echo is that turn's anchor and is written first.

Prompt journaling moves to its own module, verbatim, to keep the translator
clear of the line cap.

* refactor(native-chat): declare the turn open at each content site

The resumed-turn rule was a predicate that re-derived whether a frame had
produced anything, duplicating work the frame handler had already done. The
content sites know: each one now calls an idempotent ensureTurnOpen before it
journals, and the guard against reopening a live turn lives in that one place.

Behaviour is unchanged; claude-turn-opening.ts is left owning only the send
echo, which is the one opener that anchors its turn to a user row.

* fix(native-chat): gate both turn edges on root-ness

The reopen path already refused nested output; the guard now reads before the
already-running check so both edges state root-ness first. The close path had
no nesting check at all, so a child's result would have ended the turn that
spawned it.

The two edges read parent_tool_use_id differently on purpose, and both fail
towards not over-claiming: opening needs proof of root-ness, so an absent field
opens nothing; closing needs proof of nesting, so an absent field still closes.

No real Claude stream has been observed carrying a nested result — the session
that prompted this work has none in any subagent stream — so the close-side
guard is symmetry, not a demonstrated fix.

* fix(native-chat): stop provider output reopening a turn nothing can close

Self-audit found two paths the reopen rule opened where no event could ever
settle the turn it created, leaving the row working for the life of the
session. Both now suppress reopening until an accepted send lifts it:

- a frame arriving after the session ended, when no event will settle anything
- a turn the provider failed, or the user stopped, where the next thing the
  provider says is not a resumption

Each has a one-lever ablation: removing the suppression read alone fails
exactly those two tests, and both fail as working-instead-of-idle.

* fix(native-chat): open a resumed turn from its first streamed delta

Streamed deltas short-circuit before the frame handler, so a resumed turn
whose first output is streamed text — the common case, since partial messages
are a pinned launch contract — kept reading idle while its partial text was
already journaled and visible. The streamed path now opens the turn too.

Also from the same audit:
- a nested result no longer swallows its own failure diagnostic; the turn gate
  now guards only settlement, and the provider-fallback row is written either way
- root-ness treats an absent parent_tool_use_id as root, so a build that omits
  the field cannot silently stop opening turns
- the suppression latch only ever sets on a failed result; a later clean result
  cannot lift it, and only an accepted send does

The opener moves into claude-turn-opening.ts so both entry points share one
root-then-suppression-then-idempotency order.

* fix(native-chat): preserve resumed-turn lifecycle semantics
2026-09-14 00:19:39 -07:00
..
2026-05-31 05:55:04 -07:00
2026-09-03 17:32:59 -07:00