Commit Graph
6694 Commits
Author SHA1 Message Date
Brennan Benson 26e1ccd3bc refactor(native-chat): require Claude's isReplay marker, drop the shape fallback
The fallback was defensive code that kept finding new ways to be wrong: it
consulted a list `src/shared/harness-injected-user-turns.ts` maintains for
prompt-derived UI (so a UI edit silently changed which frames could claim a
send's identity), it rejected a genuine echo whose own text opened with a
harness shape, its string-content branch returned before the tool-result
rejection, and the latch that switched it off lived on the session object - so
it reset on every reconnect and re-opened the original bug once per connection.

Measured against the real CLI instead: `--replay-user-messages` is
unconditional in CLAUDE_STRUCTURED_BASE_ARGS, and every echo it produces is
stamped - verified for text, image-only and text+image sends. A slash command
may get no user echo at all, which `acceptsResult` already settles on the
command's result receipt.

So the marker is the whole test. No content sniffing, no shared list, no latch,
no per-reconnect window: -98 lines.

The fake CLI in claude-structured-session-adapter.test.ts echoed sends WITHOUT
`isReplay`, unlike the real binary. That double is what made a content-sniffing
gate look necessary; it now stamps like the real thing.

Found by the review loop on #15657.
2026-08-20 18:12:54 -07:00
Brennan Benson 67785d2a08 fix(native-chat): retire the failing dispatch's own waiter, not the queue head
`dispatchClaudeTurn`'s error path shifted `dispatchWaiters[0]`, which is only
this call's waiter when nothing else is in flight. With two sends queued, a
broken pipe on the SECOND resolved the FIRST with null - reporting a delivered
message as "delivery unconfirmed" while the send that actually failed stayed
queued for its full ack budget, and the first send's replay then settled a
waiter nobody awaited.

Same mis-adoption class this PR is hardening: settle a dispatch only on
evidence that belongs to it. `waitForReplay` now hands its waiter back so the
error path can splice that one, mirroring what the timeout path already did.

Also raises the tests' ack budget to 5s. vi.waitFor's first successful poll
lands ~50ms after the waiter is armed, so a 100ms budget left almost nothing
for the frame and could expire under parallel load.

Found by the review loop on #15657.
2026-08-20 18:01:23 -07:00
Brennan Benson 61adf0878e test(native-chat): make the image-replay case guard the branch it names
The frame carried `isReplay: true`, so the gate short-circuited and
`claudeUserFrameText` never ran - the content branch the test exists for was
uncovered. Verified by mutation: making claudeUserFrameText return null when no
part carries text (exactly the regression the comment warns about) left the
test green.

Drop the marker so the frame takes the fallback, and give this one case a 5s ack
budget: it does real file I/O before the timer starts, so 100ms could expire
under parallel load before the frame arrives.

The same mutation now fails the test.

Found by the review loop on #15657.
2026-08-20 17:50:14 -07:00
Brennan Benson 341ea87dce fix(native-chat): stop the injected-turn allowlist being load-bearing
The shape fallback ran on every frame without `isReplay`, so an injected turn
whose leading text is not on the hand-maintained allowlist in
harness-injected-user-turns.ts would still steal the send identity - and an
injected turn upserts a body, so it overwrites the user's own bubble before the
real replay appends a second one.

The launch args always request replays, so once this session has seen the CLI
stamp `isReplay`, a frame without it cannot be the echo. Latch that and switch
the fallback off; it now only ever runs on a CLI that never stamps the marker,
where the alternative is every send timing out into a false
"delivery unconfirmed" and being sent twice on retry.

Also corrects the string-content comment: `readClaudeMessageEnvelope` tolerates
a plain-string `content` but builds an EMPTY envelope from it, so such an echo
settles the dispatch without upserting a body - the same as an image-only echo,
not the reconciliation the old wording implied.

Found by the review loop on #15657.
2026-08-20 17:36:28 -07:00
Brennan Benson c0ee83c8d0 fix(native-chat): trust Claude's own isReplay marker for the send identity
"Not a tool result" was too weak. Claude publishes harness-injected turns -
interruption notices, system reminders, local-command output - in the same
top-level `user` shape with a null parent_tool_use_id, and those are the WORSE
half of the class: unlike a tool result they do build a journal body, so
adopting one upserts harness text over the user's own bubble and still leaves
the real replay to append as a second row.

`--replay-user-messages` stamps `isReplay` on the genuine echo, so trust that
when present. Keep a shape test as the fallback for a CLI that omits the field,
now excluding harness turns via the repo's existing
`isKnownHarnessInjectedUserTurnText` rather than a second enumeration.

Also accept string `content`. `readClaudeMessageEnvelope` already tolerates it,
so rejecting it was a new way for a send to go unacknowledged - which times out
into a false "delivery unconfirmed" and sends the message twice on retry.

Found by the review loop on #15657.
2026-08-20 17:25:31 -07:00
Brennan Benson 54d653240d fix(native-chat): gate the Claude replay waiter on shape, not on a modeled body
Requiring `claudeMessageBody(...) !== null` inherited that function's content
model: it builds blocks only for text and URL images, while dispatch
base64-encodes local image paths. An image-only send was therefore refused its
OWN replay, ran out the 10s ack timeout, and reported a delivered message as
`unknown` - which offers retry, and retry sends it to Claude a second time. A
duplicate render traded for a duplicate send.

Reject the tool-result shape directly instead. That still keeps a tool-result
turn from stealing the identity, without making confirmation depend on which
content parts the translator happens to model.

Measured against the real CLI while checking the timeout concern: a queued
send's replay is published at the FIRST tool boundary after it is queued, 2-3ms
after that boundary's tool result (1004ms queued -> 4772ms tool_result ->
4774ms replay, across a 22s three-tool turn). So rejecting tool results costs no
time - whenever there was a frame to adopt, the right one follows immediately -
and a turn with no boundary inside the timeout times out identically either way.

Found by the review loop on #15657.
2026-08-20 17:17:49 -07:00
Brennan Benson c266acc215 fix(native-chat): adopt only the real user replay as a Claude send's identity
Claude publishes a top-level `user` turn for tool results as well as for the
`--replay-user-messages` echo of the prompt Orca dispatched. Both carry
`type: "user"` and a null `parent_tool_use_id`, so resolveClaudeReplayWaiter
adopted whichever arrived first.

A send issued while a turn is running is dispatched with tools in flight, so
the tool-result turn takes the waiter. Dispatch then records that uuid as the
provider identity, the journal aliases the submission onto an item nothing
ever upserts, and the real replay lands as its own user item - the user's
message renders twice.

Gate the waiter on the same body claudeMessageBody builds, so the identity
dispatch adopts is always the one the echo upserts into. An unrecognized
frame leaves the send `unknown` ("delivery unconfirmed"), never duplicated.

Both frames in the regression test are captured verbatim from
`claude -p --input-format stream-json --output-format stream-json
--replay-user-messages`; the previous double invented a `user` frame with no
message body at all, which the CLI never emits.
2026-08-20 15:51:52 -07:00
Brennan Benson 10f565017e fix(native-chat): recover Claude outbox and resume state 2026-08-14 14:07:20 -07:00
Brennan Benson 53b2b51d60 Merge remote-tracking branch 'origin/brennanb2025/codex-structured-mobile-write' into brennanb2025/claude-structured-mobile
# Conflicts:
#	src/main/native-chat/agent-session-wire/structured-agent-session-handoff-reverse.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-handoff-types.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-handoff.test.ts
#	src/main/runtime/orca-runtime.ts
2026-08-13 13:29:28 -07:00
Brennan Benson 84727e695a fix reverse structured session handoff 2026-08-13 12:43:05 -07:00
Brennan Benson ecdd5c8dd9 fix structured session TUI handoff recovery 2026-08-13 11:22:13 -07:00
Brennan Benson ec43fdf43d Merge commit '114eee22dd9fb11f5e814dad36d5298d6768c57e' into brennanb2025/claude-structured-mobile
# Conflicts:
#	mobile/src/session/mobile-structured-tui-send.test.ts
#	mobile/src/session/use-mobile-structured-session-options.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-handoff.test.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host-handoff.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
#	src/main/runtime/orca-runtime.ts
#	src/main/runtime/structured-tui-process-identity.test.ts
#	src/main/runtime/structured-tui-process-identity.ts
#	src/renderer/src/runtime/local-structured-session-tabs-sync.test.ts
#	src/renderer/src/runtime/local-structured-session-tabs-sync.ts
2026-08-13 11:18:15 -07:00
Brennan Benson 114eee22dd fix structured provider visibility and request handling 2026-08-12 21:59:58 -07:00
Brennan Benson 414129d4b2 fix(native-chat): preserve Claude question groups 2026-08-12 21:04:49 -07:00
Brennan Benson b8d906228f fix structured outbox and tab restore races 2026-08-12 20:27:38 -07:00
Brennan Benson e444d4bedd fix(native-chat): drop local terminal topology from structured sync 2026-08-12 19:36:51 -07:00
Brennan Benson 1377932db3 fix(native-chat): keep structured recovery provider-neutral 2026-08-12 18:46:05 -07:00
Brennan Benson 20c2deda29 fix(native-chat): queue stale idle TUI handoffs 2026-08-12 18:42:39 -07:00
Brennan Benson 4446a4681a fix(native-chat): wait for structured TUI process proof 2026-08-12 18:41:13 -07:00
Brennan Benson 6f30927a6c fix(i18n): catalog structured session recovery control 2026-08-12 18:40:21 -07:00
Brennan Benson c23139423a fix(native-chat): gate restart recovery publications 2026-08-12 18:39:52 -07:00
Brennan Benson a03beee134 fix(native-chat): revalidate restored and retried owners 2026-08-12 18:39:16 -07:00
Brennan Benson 35fb039187 fix(native-chat): validate pinned launch environment 2026-08-12 18:38:23 -07:00
Brennan Benson e2b3600e3b fix(native-chat): close handoff admission races 2026-08-12 18:37:56 -07:00
Brennan Benson 62b26c58b9 fix(native-chat): serialize structured handoff recovery 2026-08-12 18:37:10 -07:00
Brennan Benson 2d3b082ab2 fix(native-chat): harden shared handoff recovery 2026-08-12 18:36:05 -07:00
Brennan Benson 53a3a9442b fix(native-chat): preserve handoff launch authority 2026-08-12 18:32:07 -07:00
Brennan Benson 8b2601b2f2 fix mobile structured first-send hydration race 2026-08-12 18:26:18 -07:00
Brennan Benson 90e3ead5cb fix(native-chat): keep Claude option controls idle 2026-08-12 18:24:50 -07:00
Brennan Benson 15ebaa1d74 Merge commit 'd53823acc7' into brennanb2025/claude-structured-mobile
# Conflicts:
#	mobile/src/session/use-mobile-structured-session-entry.ts
#	mobile/src/session/use-mobile-structured-session-options.test.ts
#	mobile/src/session/use-mobile-structured-session-options.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-attach-flow.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-handoff-forward.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-handoff.test.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host-handoff.ts
#	src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
#	src/main/runtime/orca-runtime-structured-tui-tab-binding.test.ts
#	src/main/runtime/orca-runtime.ts
#	src/shared/agent-session-record.ts
2026-08-12 18:06:39 -07:00
Brennan Benson d53823acc7 test(native-chat): give the host-oracle matrix test an explicit timeout 2026-08-12 17:40:41 -07:00
Brennan Benson 5fc6f7a1cc test(native-chat): derive refusal retry state from host 2026-08-12 17:35:57 -07:00
Brennan Benson 2355a20b29 fix(native-chat): rotate refused send operations 2026-08-12 17:21:10 -07:00
Brennan Benson d82d807180 fix(native-chat): rotate settled handoff operations 2026-08-12 17:04:49 -07:00
Brennan Benson 7f4a1d13f7 fix(native-chat): replay pending option mutations 2026-08-12 16:54:16 -07:00
Brennan Benson 973dd58aba fix(native-chat): preserve options across structured handoffs 2026-08-12 16:40:09 -07:00
Brennan Benson 2c8b009101 fix(native-chat): hydrate resumed structured options 2026-08-12 14:16:00 -07:00
Brennan Benson 5971fcb437 fix(native-chat): persist structured session options 2026-08-12 13:58:48 -07:00
Brennan Benson 1cf45caa24 fix(native-chat): route structured Codex options directly 2026-08-12 12:43:24 -07:00
Brennan Benson 5c2d2bf6e3 fix(native-chat): queue stale idle TUI handoffs 2026-08-12 02:58:46 -07:00
Brennan Benson f5cfa241a1 fix(native-chat): wait for structured TUI process proof 2026-08-12 01:54:47 -07:00
Brennan Benson b44714fe56 fix(i18n): catalog Claude session controls 2026-08-12 01:04:10 -07:00
Brennan Benson 2f8fc3cb4d fix(native-chat): gate restart recovery publications 2026-08-12 00:47:46 -07:00
Brennan Benson 22312a8932 fix(native-chat): revalidate restored and retried owners 2026-08-12 00:28:12 -07:00
Brennan Benson e5bd9812c9 fix(native-chat): validate pinned launch environment 2026-08-12 00:09:23 -07:00
Brennan Benson 8f39ca84b6 fix(native-chat): close handoff admission races 2026-08-11 23:52:14 -07:00
Brennan Benson 7da8def904 fix(native-chat): serialize structured handoff recovery 2026-08-11 23:32:07 -07:00
Brennan Benson 7371c0d2f5 fix(native-chat): harden Claude handoff recovery 2026-08-11 22:59:34 -07:00
Brennan Benson 7b3144c037 Merge remote-tracking branch 'origin/brennanb2025/native-chat-toggle' into brennanb2025/codex-structured-mobile-write 2026-08-11 22:57:35 -07:00
Brennan Benson 6456b4cb5a fix(native-chat): recover TUI transcripts read-only 2026-08-11 22:25:32 -07:00