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