Skip to content

fix(native-chat): chat failure messages appear in the app's language - #23674

Merged
brennanb2025 merged 61 commits into
mainfrom
brennanb2025/chat-failure-words-i18n
Sep 30, 2026
Merged

brennanb2025 merged 61 commits into
mainfrom
brennanb2025/chat-failure-words-i18n

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 11 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1096 $\color{#cf222e}{\Huge{\mathbf{−}}}$​52 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1044
Prod 30 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1150 $\color{#cf222e}{\Huge{\mathbf{−}}}$​189 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​961

ELI5

In the Spanish, French, Japanese, Korean and Chinese app, native chat said its failures in English: "Claude couldn't start.", "Your message was not sent because…", "Run /compact again." Now the desktop says them in the app's language. Separately, a /clear or /compact on a chat whose agent was at rest lost its own answer, so the chat either said nothing or said "Conversation operation was not confirmed." That's fixed too.

#23935 (instant /clear) depends on this: its /clear stops a running agent, which moves the fence during the request.

What it fixes, and where

  • Surface: native (structured) chat on desktop. Mobile changes in one place only (below). The host's saved chat rows, the terminal, and anything the host writes are unchanged.
  • Failure words in the app's language. These now follow the app language: the notice under a message that wasn't sent, the caption on a queued message that came back unsent, the line under the composer after a failed /clear or /compact, the launch-retry strip, and the history pane's "couldn't open this chat's history" state.
  • A command's own answer is no longer thrown away. Each chat pane has a "fence": a number the host bumps every time the chat's agent process starts or stops. The pane drops any answer issued under an older fence, because that answer was about a process the chat no longer runs. A /clear or /compact on an at-rest chat starts the agent itself, so its own answer always arrived under an older fence and was dropped. On main, a /compact on an at-rest chat showed "Conversation operation was not confirmed." and left "/compact" in the composer. A /clear could leave the screen unchanged.
  • Not affected: chats in the terminal, host-saved rows (see Known weaknesses), and every write other than a conversation command, which keeps the exact-fence rule.

What Changed

  • Failures are worded from their type, not from an English string. The host already sends a typed failure fact beside its English sentence: what failed, which agent, and for a command, which command. Every sentence is now built from whole pieces in src/shared/agent-session-failure-copy.ts, such as "{{agent}} couldn't start." and "Run /{{command}} again.". The host fills them in English exactly as before; its output is byte-for-byte unchanged across every failure kind. Desktop translates each piece whole, with the English as its fallback (agent-session-failure-words-text.ts).
  • A fact this build can only partly read shows the host's own sentence. readWholeAgentSessionFailureFact decides this, when a newer host adds a reason this build doesn't know. Rewording a partial fact would drop the host's advice, for example "Start a new chat to continue." This applies everywhere: rejected-message notices, the returned queued-message caption on desktop and on mobile, and command replies.
  • Sentences join the way the language does. joinSentences puts no space after 。 in Japanese and Chinese, and a space elsewhere.
  • The history pane says it once. When its title already says this chat's history didn't load, the line under it says only "Orca keeps trying to load it." Under the "saved by a newer Orca" title, the line keeps its full sentence so "it" has something to refer to.
  • The command-answer rule (use-structured-agent-session-mutate.ts): only the answer to the conversation command this pane is still waiting on outlives a fence move. The pane remembers that one request; a newer request replaces it, and disabling the pane forgets it. Every other answer from an older fence is still dropped, and any answer after the pane closes updates nothing.
  • A failed command's line stays empty when the chat already says why. If the command's failure is the chat's own failed start and its row is on screen, the line under the composer stays empty, as for a message that start rejected. /clear is excluded, because its failed start belongs to the new chat, not this one.
  • 57 new English keys, hand-translated into es, fr, ja, ko and zh. One key nothing uses any more was removed: "Conversation operation was not confirmed."

Why

  • Word from the fact, not the sentence. Translating the host's finished English sentence would need the host to send every string it might ever say, or would break the moment it rewords one. Wording from the typed fact keeps the host as the single source of what happened, and lets every reader say it in its own language.
  • The English lives in one place. The host and desktop build sentences from the same pieces, so they can't drift. A test checks every piece against the English catalogue, and a piece with no translation key fails to compile.
  • Why a per-request answer and not "any answer while this chat is shown". A command is the only write that moves the fence under its own answer, so it needs the exception. Tying the exception to the one request the pane waits on keeps it as narrow as possible: no older answer can be applied as if it answered the current one.

Differences from the common pattern

  • Intended: failures are worded from their typed fact and shown in the app's language. The common pattern shows the provider's or host's English error text as-is, with no UI translation. Orca ships Spanish, French, Japanese, Korean and Chinese, and English failure lines in those apps are the observed failure this PR fixes (screenshots below). It stays safe because the host still sends its English sentence, and a fact this build can only partly read shows that sentence unchanged.
  • Intended: a conversation command's answer outlives a fence move only while this pane still waits on that one request. The common pattern delivers an answer to the call that awaits it and has no fence. Orca's panes drop answers from an older fence because they describe an agent process the chat no longer runs; a /clear or /compact is the one write that moves the fence under its own answer, so it gets this narrow exception. The effect matches the common pattern: an answer reaches the request that asked for it, and an answer to a replaced or abandoned request is ignored.

Linked Issue

N/A (internal).

Visual Proof

Before is main; after is this branch (the held-messages shot is from an earlier branch head whose wording code is unchanged since). Both were run live in French on macOS, with an agent stand-in whose start always fails.

History pane, when a chat's history can't be read. Before, the line under the title repeated the title; after, it only says Orca keeps trying:

BEFORE fr history pane
AFTER fr history pane

A queued message's own notice when the history can't be read. Before it was English, "Claude stopped before it finished starting."; after it's French:

BEFORE fr held messages, journal unreadable
AFTER fr held messages, journal unreadable

/compact whose start fails. Before, "L'opération de conversation n'a pas été confirmée." appeared under the composer; after, nothing appears there, and the chat's rows say what happened:

BEFORE fr after /compact
AFTER fr /compact whose start failed

/clear on an at-rest chat. Before, the screen could stay unchanged; after, the pane moves to the new chat, whose own row explains its failed start:

BEFORE fr after /clear
AFTER fr /clear

A queued message returned unsent, captioned in French. The queue was switched on locally for this capture; it is off on main:

AFTER fr returned queued-message caption

Testing

  • I manually tested these changes locally: live Electron on macOS (M4 Air), in French and Japanese, with an isolated home and an agent stand-in whose start always fails.
  • Automated tests added/updated.
  • Unit tests: 15 targeted files, 197 tests passing at the final head, covering failure words in every language, the command answer across a fence move (including an answer the pane stopped waiting on, and a /clear that stops a running agent), the start-row silence, the returned-message caption and the history pane.
  • Each test was checked by removing its fix and watching it fail. Main's fence rule turns 8 command tests red; keeping every command answer turns 3 red; the history-pane and mobile-caption fixes each turn their tests red.
  • The web typecheck passed. Lint and the changed-code quality gate are clean. The localisation catalogue checks pass.
  • Host output: every failure kind was checked across 15 fact shapes and 5 contexts, 4,200 cases in all, with 0 differences between main and this branch.
  • Not verified live: a /compact that starts successfully, because the stand-in can only fail. SSH, Windows and Linux were not run live either; the change is renderer and shared wording only, with no host, wire or platform code.

Known weaknesses

  • Chat rows the host saved stay in the host's English. Examples: a failed-start row, or "Claude is rate-limited and retrying.". Rewording them on desktop needs the host to save more context, for example whether the failure followed /clear. Every row sentence already has a translation key.
  • Some compaction answers carry no failure fact ("The command has not started yet.") and still show English. That was already so on main.
  • The Spanish catalogue lacks "Wait for pending work…" (conversationCommand.pendingWork). That was already so on main.

Needs native review

The new components.native-chat.failureWords.* keys and components.native-chat.state.error.keepsTrying, hand-translated into Spanish (es), French (fr), Japanese (ja), Korean (ko) and Chinese (zh).

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

…its reason

History, subscribe, snapshot and options reads reach a chat through one accessor, whose open had no
catch: a journal that would not open reached every client as a runtime error carrying the storage's
own text (a path, "file is not a database"). The accessor, and the options read's own open, now throw
the classified journal refusal: journalCorrupt when SQLite reports damage, journalUnavailable
otherwise. The storage text goes to the log only.

The wire code stays runtime_error and the message becomes the bare code, as for every thrown
refusal; the reason rides in the error's data.
…no hand-written reason

The sweep passed an English sentence as the stop's reason. It lived only in memory and nothing read
it: the delivery loop words the error row and the rejection from the hostStopped fact. Dropped, with
the display-name lookup that built it.
…never its message

A thrown agent-session refusal reaches the client as runtime_error with the bare code as its message
and the typed refusal in error.data. Stop, answers, options and goals, a launch's held option pick,
the option picker's failure toast, and the Retry line of a chat that could not start now word it
from that refusal through the shared notice table. What each caller decides about the outcome is
unchanged: only the words move.

The Retry line of a failed start no longer prints the host's message or a thrown error's text; it
keeps the refusal as a fact and says the cause and step its reason names, or only that the chat
could not be started. The option toast keeps a local option surface's own sentence.
…nd damage stops the retry

The structured chat's read failure showed the host's text on the status line, and the pane always
said Orca keeps trying. The read transport now takes the refusal from the error's data (a stream
payload or a thrown RPC error), the reducer keeps it beside the failure text, and the pane and the
status line word it through the notice table, once: on the pane when the failure took it, else
beside the transcript that stays.

A damaged journal says "Unable to load this chat." and the read stops reconnecting for that run;
reopening the chat reads again. An open that can clear names its cause without "Try again", since
the pane retries on its own. A failure that names no reason keeps today's generic line. Finality
comes from the refusal's reason, never its message, which is the bare code for both.
A message the host recorded and then rejected keeps the host's typed fact beside its reason, but
the Retry words re-read the reason alone. Now the fact decides: a hand-over failure says Orca
couldn't reach the agent, a kind whose reason may be a legacy marker gets its fact's own sentence
(a full queue now says so instead of only "not sent"), and any other kind shows the sentence the
host wrote for it, which carries the agent's name and any words the provider wrote for a person. A
row with no fact reads as before.
…s own row

The structured chat showed one Retry strip under the transcript for whichever single entry it
picked, so a second failed message had no reason and no Retry of its own. The terminal-backed
chat's existing per-row delivery marker now carries a notice and an optional Retry, and the
structured pane derives one per message from the outbox on each render: every rejected message,
and the one the queue stopped on (read through the drain's own rule, so a Retry never names a
message waiting behind it). Each is worded from that message's stored failure. The single strip is
deleted. Nothing new is stored, and the shared message projection is untouched.
… already acts

Three wording rules for the desktop chat:

- A chat that could not start shows Retry beside its reason, so the reason stops at its cause
  where the Retry is the step: a reason whose action is to retry, and a start failure's "send
  your message again". Any other step stays (quit the terminal agent, start a new chat). The
  start-failure sentences take a retryControl context for this; what the host writes is unchanged.
- A history that couldn't open right now still reconnects, so the pane keeps "Orca keeps trying
  to load it" under its cause. Only a damaged history, which no retry reads past, drops it.
- A read failure that names no reason while the transcript is shown is only the pane
  reconnecting: the status line says "Reconnecting to this chat…" in muted text, not an error.
  New key components.native-chat.state.reconnecting, hand-translated for es/fr/ja/ko/zh.
… is moving

Each rejected message's row offered its own Retry even while the queue was stopped on another
message. Any Retry clears the stopped queue, so pressing a rejected message's Retry also sent the
message the queue was holding, which the user had not retried; behind a message whose delivery is
unconfirmed, the retried one instead went back into the queue with no notice and waited there.

While the queue is stopped, only the message it stopped on offers Retry, as the single Retry strip
this replaced did. A rejected message keeps its words on its row and gets its Retry back once the
queue moves.
…n every reconnect

A reader reconnects every 750 ms while a journal open can clear, and each attempt logged the
failure with its full stack. The read door now logs a session's failure once until that session
opens, closes, or fails differently; every attempt is still refused with its reason.
…tice stops at the cause

A rejected message offering Retry read "Claude stopped before it finished starting. Send your
message to try again." beside that button. Its row now takes the rule the launch strip already
follows: beside its own Retry the words leave out sending or trying again, worded from the stored
fact with the chat's agent name. The stored fact keeps less than the host wrote from (a refusal,
the provider's words), so a reason it cannot rebuild exactly is kept as written. A rejected
message without a Retry, while the queue is held, keeps the step. What the host writes and the
phone's notice are unchanged.
…e one the queue is held on

Retry released the queue's refusal hold whichever message it was pressed on. While a queued message waited ahead of a held one, every rejected message offered Retry, and pressing it also sent the held message the person had not retried. Retrying an unconfirmed message ahead of a held one did the same. Retry now releases the hold only for its own message.
…sign in first

Beside a Retry the notice dropped the whole next step, so a chat that could not start because the agent was not signed in read only the cause. Pressing Retry without signing in fails the same way again. The words now keep the sign-in step and leave out only the resend, which the Retry button is.
…ep the same way

A message the queue stopped on worded its refusal with no agent name and with its retry step, beside its own Retry, while a rejected message next to it named the agent and left the step to the button. The launch strip and the history pane each had their own copy of the same rule. One wording context now goes through the one notice table for every surface: a Retry beside the words, or a pane that reconnects on its own, is the step for a reason whose action is to retry, and every other step stays. What the phone and the host write is unchanged.
… fact, never by comparing sentences

A message the host recorded and then rejected kept only the rejection's kind on the message, so its notice was reworded from that smaller copy only when it rebuilt the host's sentence word for word. A different agent name, an older host's wording, or anything the copy dropped (why a start failed, the provider's own words) left the host's sentence in place, beside a Retry that repeated its resend step. The notice now reads the journal's own rejection for that message, found by id, with the pane's agent name and Retry, and shows the provider's words only when they were written for a person. The message's smaller copy words it only when that journal row is not loaded. Nothing new is stored.
… new id the first time

Whether a Retry needed a new message id was remembered in memory for one message, or read from the journal row when it was loaded. After a restart, or for an older message whose row was not loaded, the first Retry resent under the old id, the host answered with the same settled rejection, and nothing visibly happened. The message now says so itself: one the host recorded and rejected always retries under a new id, including after a restart. A refusal that already gave the message a fresh id, and a message whose delivery is unconfirmed or in flight, keep their id as before.
…eps the provider's words

When the journal row that rejected a message is not loaded, the message's own copy of the
fact has no provider detail or start refusal. For the kinds worded from those, the row now
shows the sentence the host wrote for the person instead of a thinner rebuilt one.
…aded journal row

Nothing covered the pane handing the journal's rows to the per-message notices, so a pane that stopped passing them would quietly fall back to the message's smaller copy of the rejection and show the host's sentence, resend step and all. The new case renders the pane with a rejected message whose journal row is loaded and checks that it reads that row's refusal in the chat's own agent name.
A read the host refused for a named reason put its sentence under the generic
"Could not load conversation" title, so a damaged history read as two lines
saying the same thing. The pane's own sentence now takes the title's place; a
history that can come back keeps its line saying Orca keeps trying. A failure
that names nothing keeps the generic title.
… was not sent

When an agent stopped before it finished starting, the chat showed the start's
red row ("Claude stopped before it finished starting. Send your message to try
again.") and then repeated that cause under every message the start rejected.
Each of those messages now reads "Your message was not sent." beside its Retry.

The match is made on typed facts, not on the words: the host writes the start's
row and the rejection of its queued messages from the same failure fact, and the
row is keyed by the start. The pane finds the loaded start-failure rows by that
key and shortens a message's notice only when its loaded journal submission was
rejected with the same fact. Any other rejection, or one whose submission or row
is not loaded, keeps its full notice. The row key moves to a shared module so the
host that writes it and the pane that reads it use one definition.
…less often

A read the host kept refusing (its history store could not be opened right now)
reopened every 750 ms for as long as the chat stayed open, about 40 opens every
30 seconds. Each reconnect now waits twice as long as the last, from 750 ms up
to 30 seconds, and never gives up; the first read that delivers anything starts
the wait over at 750 ms. A damaged history still stops reconnecting at once.

Reset happens on a delivered read, not on connect: a local subscribe resolves
before the host's open refuses, so resetting there would keep the 750 ms loop.
The read retry doubles its wait up to 30 s during an outage, and nothing
reset it when the remote runtime reconnected, so the transcript could
lag the reconnect by up to 30 s. The read now watches the runtime
status store's contact-regained edges (hostContactEpoch for a
same-runtime return, connectionGeneration for a new runtime session)
and, when one lands, runs a waiting retry immediately with the wait
reset to its base.
Every sentence agentSessionFailureWords writes is now assembled from a
table of whole English pieces, so a reader can supply its own words for
each piece. The host still fills them in English, byte for byte as
before.
A refused start and a rejected message now carry their failure fact to
the notice instead of its English sentence, and desktop words that fact
through translate keys whose English defaults are the host's own
pieces. The host keeps writing English into rows and reasons, and a
host sentence with no fact beside it still shows as written.
When the pane's title already says Orca couldn't open this chat's
history right now, the line under it no longer repeats that the
transcript could not be read; it says only that Orca keeps trying to
load it. The pane with no named reason keeps its two-part line.
…s space

Sentences were joined by the UI language: no space in Japanese and Chinese,
one elsewhere. A plugin pack for a Chinese or Japanese variant that predates
the failure words falls back to English for them, so a notice read
"您的訊息未傳送。Claude couldn't start.Send your message to try again."

Each gap now follows the sentence before it: none after a full-width 。!?,
one space after anything else. The built-in Japanese and Chinese catalogs end
every sentence in 。, so they read as before, and English is unchanged. Since
the rule no longer needs the language, one joiner serves every surface and
the failure-sentence builder no longer takes one alongside its words.
…shows the host's own sentence

The line under the composer words a failed /clear or /compact from the fact
the host sends beside its sentence. The reader drops any part this build
cannot place, such as a refusal code a newer host added, and the rest of the
fact can then give different advice: "Run /clear again." where the host said
"Start a new chat to continue."

When any part the host sent did not survive the read, the line now shows the
host's sentence as written, the same as for a host that sends no fact. A fact
this build reads whole is still worded in the app's language.
…the host's sentence everywhere

The previous check compared only a fact's top-level parts, so a known refusal
code carrying a reason a newer host added still counted as read: the reader
dropped the reason and the notice re-worded what was left, which can advise
differently from the host ("Run /clear again." against "Start a new chat to
continue.").

One shared reader now answers whether this build read the whole fact: it reads
the fact and keeps it only when the read equals what arrived, at every depth.
Every place that chooses between wording a fact and showing the host's text
uses it: the line under the composer after /clear or /compact, a rejected
message's notice from the journal's fact, and the smaller copy a rejected
message keeps for when its journal row is not loaded. Matching a rejected
message to the start row that already says why still uses what this build can
read, since that is identity, not wording. Facts this build reads whole are
worded as before.
…lure-words-i18n

Main now names the waiting conversation command in a failed start's next step and adds
the refused-command and refused-Stop sentences. Each becomes whole pieces desktop can
translate: "Run /{{command}} again." and its sign-in and account forms replace the
/clear-only pieces, and the new sentences get their own keys in every catalog. A
newer-Orca history cause joins the causes that already say the history can't be read.
…n desktop too

The host names the command a failed start was waiting on, for /clear and /compact alike.
The line under the composer re-worded only /clear with it, so a /compact whose start failed
read "The agent couldn't restart. Send your message to try again." in the reader's
language. It now words every command the host answers with the agent and the command the
host used, so desktop English matches the host and French or Japanese keep /compact.
…eration was not confirmed

A /compact on a chat whose agent is not running starts it first, and that start takes a new
lease, so the chat's fence moves before the command's reply arrives. The write settles as one
for a fence this pane no longer shows, and the composer line read that as "Conversation
operation was not confirmed." Such a write now says nothing there, as every other write
already does: the chat's own start-failure row says why, and the command's message is
rejected in the journal. The sentence it printed is gone from the catalogs.
…the fence

A /clear or /compact on a chat whose agent is at rest starts the agent first, and that start
moves the chat's fence before the command's reply arrives. Every reply from an earlier fence was
discarded, so a /clear whose new chat failed to start said nothing at all, and a /compact that
started left "/compact" in the composer.

A conversation command's reply is now kept while the pane still shows the chat it was sent for;
a closed pane or another chat still drops it, and every other write keeps the fence rule. The
line under the composer says nothing only when the failure is the chat's own start and that
start's loaded row already says why, the rule a message that start rejected already follows.
… for a fact read in part

The caption under a returned queued message re-worded its failure from whatever this build could
read of the fact. A newer host's fact with a refusal code or reason this build drops read as a
shorter sentence with different advice. It now re-words only a fact read whole, and otherwise
shows the host's own sentence, as every other surface that re-words a fact does.
…nd /compact

The fence-move tests now state the rule as the code has it: a conversation command's reply is kept
whenever the pane still shows its chat, whatever moved the fence. They cover a /clear that
completed, and a failed start for each command in French with the fact that command really meets
(a /clear's new chat fails to start; a /compact's chat fails to restart).
…ce move

A conversation command's reply was kept across a fence move whenever the pane still showed the
same chat. A reply the pane had stopped waiting on, because it left the chat and came back or a
newer command replaced it, was applied as if it answered the current one.

The pane now remembers the one command request it waits on; only that request's reply is kept
after the fence moves, and closing the pane or showing another chat forgets it. Tests now drive the
fence move during the request itself rather than through /clear starting an agent, and cover a
/clear that stops a running agent.
…, and the phone shows the host's words for a fact it reads in part

When a chat's history was saved by a newer Orca, the pane's title reads "Chats were saved by a newer
Orca. Update Orca to keep using them." and the line under it said only "Orca keeps trying to load
it.", with nothing for "it" to mean (in French and Spanish the pronoun also disagreed with "chats").
That title names every chat rather than this one, so the pane keeps the full line: "The transcript
could not be read. Orca keeps trying to load it."

On the phone, a returned queued message whose failure fact this build reads only in part was
re-worded from what it could read, dropping advice the host gave; it now shows the host's own
sentence, as the desktop card already does.

The comment on the reply the pane waits on now says what forgets it: a newer command, or disabling
the pane.
@brennanb2025
brennanb2025 marked this pull request as ready for review September 30, 2026 08:31
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status at f7771bb: ready for review (not merged).

The problem. In the Spanish, French, Japanese, Korean and Chinese app, native chat said its failures in English: the notice under an unsent message, a returned queued message's caption, the launch-retry strip, the history pane, and the line under the composer after a failed /clear or /compact. Separately, a /clear or /compact on a chat whose agent was at rest threw its own answer away. The command starts the agent, which bumps the chat's fence (the number that marks which agent process the pane is talking to), and the pane dropped every answer issued under an older fence. So /compact said "Conversation operation was not confirmed." and left "/compact" in the composer, and /clear could leave the screen unchanged.

What changes for the user. Those failure sentences now appear in the app's language, and Japanese and Chinese join sentences without a space. A /clear or /compact answer is no longer lost: the "not confirmed" line is gone, /clear moves the pane to the new chat, and a failed command explains itself once. Host-saved chat rows are unchanged and stay English (see Known weaknesses in the body).

Fixed during review

  • The command-answer rule was narrowed to the approved one: only the answer to the command this pane is still waiting on outlives a fence move, tracked per request. The earlier rule kept any answer while the same chat was shown.
  • The phone's returned-message caption now shows the host's own sentence when it can only partly read the failure, as desktop does. Before, it dropped the host's advice, e.g. "Start a new chat to continue.".
  • Under the history pane's "saved by a newer Orca" title, the line below said "Orca keeps trying to load it." with nothing for "it" to refer to (and wrong gender in French and Spanish). It now keeps its full sentence.
  • Comments that described the command-answer rule inaccurately were corrected.

Deferred (P3 or already on main)

  • Some compaction answers carry no failure fact ("The command has not started yet.") and stay English. That was already so on main, and fixing it needs the host to send a fact.
  • Spanish lacks conversationCommand.pendingWork. That was already so on main.
  • One comment still mentions "left the chat" in the command-send code, a case only a test can produce (nit).

Verified

  • Three independent code reviews (the command-answer rule, the wording core and translations, and the UI surfaces), a narrow re-review of every fix, and a pre-release readiness checklist. No P0–P2 issues remain.
  • No answer can land on the wrong chat: each chat has its own pane instance, and the fence it reads belongs to that instance only.
  • The host's English output is identical to main across 4,200 cases, so older clients see exactly what they saw before.
  • 197 targeted unit tests pass. Each fix was checked by removing it and watching its tests fail. The web typecheck passes, and CI is fully green at f7771bb.
  • Live Electron on macOS (M4 Air), in French and Japanese, with an isolated home and an agent stand-in whose start always fails. Checked: /compact whose start fails (no "not confirmed" line, and the rows explain); /clear (the pane moves to the new chat); a returned queued-message caption in French (with the queue switched on locally only); the history pane; and the Japanese sentence join. Screenshots are in the PR body.

Not verified

  • A /compact that starts successfully, live: the stand-in can only fail.
  • The changed mobile test was run from a copy, not in place (it passed). PR CI doesn't run mobile tests; they run on main.
  • SSH, Windows and Linux were not run live. The change is renderer and shared wording only, with no host, wire or platform code.

Behaviour to know about. After a /compact whose start fails, the composer clears if the host accepted the command before the start failed, and keeps "/compact" if the start failed first. This is the same rule a message follows: accepted clears the box, refused keeps it. Either way, the chat's rows say to run /compact again.

@coderabbitai

coderabbitai Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 266bff24-594e-4cce-977e-f21c328347f0

📥 Commits

Reviewing files that changed from the base of the PR and between 63caa6e and 30bcac2.

📒 Files selected for processing (6)
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/fr.json

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

The change adds shared English failure-copy templates and localized renderer translations. Structured notices render failure facts at display time, and returned-message paths retain complete readable facts or host-provided sentences. Conversation-command replies receive updated outcome handling and can settle across a session fence change. Read-history errors also identify when their headline already says history failed to load, so the empty state can show distinct retry copy.

Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to 30bca

The pending-work notice uses the configured English fallback when Spanish copy is absent; no material regression in the reviewed changes remains.

Architecture Summary

Architecture risk: 🔵 Low · up to 30bca

The change affects 2 systems.

Changed systems: src, mobile

Architecture concerns
No architecture-level concerns identified.

Review details

Systems and components

  • observed — src (ui) was modified; 39 changed files map to changed impact.
  • observed — mobile (service) was modified; 2 changed files map to changed impact.

Before / after behavior

  • observed — Modified behavior in mobile/src/session/mobile-structured-queued-message-cards.test.ts: Adds a test using a returned message with a startFailed rejection and an unrecognized reason, asserting that the card caption uses the host-provided sentence.
  • observed — Modified behavior in mobile/src/session/mobile-structured-queued-message-cards.ts: The failure-fact import changes from readAgentSessionFailureFact to readWholeAgentSessionFailureFact.
  • observed — Modified behavior in mobile/src/session/mobile-structured-queued-message-cards.ts: returnedCaption now passes the complete failure fact from the rejection into the write-notice formatter, replacing the prior single-fact lookup; its reason fallback and retry-control settings remain unchanged.
  • observed — Modified behavior in src/renderer/src/components/native-chat/NativeChatEmptyState.tsx: The component now imports NATIVE_CHAT_KEEPS_TRYING_COPY alongside the existing empty-state copy.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 48 functions across 34 files. (6 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: localizing native-chat failure messages in the app's language.
Description check ✅ Passed The description is detailed and mostly complete. It covers the user impact, implementation, rationale, testing, visual proof, limitations, and platform coverage. It marks the linked issue as N/A for a…
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 43.75% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 48 functions across 34 files. (6 skipped: 6 unsupported.)

  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 4/5

[Medium risk] Rewires chat failure messages to use the app's language.

The PR does not appear safe to merge until an old start-failure row can no longer hide a later /compact failure.

Findings

  1. P1 Old failures hide new ones ▶

Summary

The PR translates native-chat failure messages from typed failure facts and preserves a conversation command’s reply when that command moves the session fence. It also adjusts history-pane and queued-message failure copy. No new finding was identified since the previous review, but the earlier command-error finding remains outstanding.

Reviews (3) · Last reviewed commit: "Merge remote-tracking branch 'origin/mai..."

Comment on lines +49 to +53
if (
value.command !== 'clear' &&
agentSessionFailureStatedByStartRow(value.failure, input.startFailures())
) {
return { accepted: false, error: null }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Old failures hide new ones

If a chat has an earlier start-failure row and a later /compact fails to start for the same reason, this check treats the old row as an explanation of the new failure. Start-failure rows remain in the journal, and the check compares their failure facts without checking which start produced them. It then hides the new command’s error, leaving /compact in the composer with no explanation beneath it.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No blocking issues. One narrow correctness gap in the new fence exception (inline), plus a latent caveat about the whole-fact guarantee.

Reviewed changes

  • Failure copy built from typed pieces — src/shared/agent-session-failure-copy.ts holds every sentence piece; the host fills them in English (sayAgentSessionFailureEnglish) and desktop translates each piece whole (agent-session-failure-words-text.ts) with that English as fallback, so the two cannot drift.
  • Partial facts fall back to the host's sentence — readWholeAgentSessionFailureFact returns a fact only when structuralValuesEqualIgnoringUndefined proves this build read all of it; used by the mobile caption, structuredAgentSessionRejectedFailure, desktop delivery notices, and command replies.
  • Language-aware joining — sentence-joining.ts drops the space after 。!?, used by failure words, write notices, and the launch-retry strip.
  • A conversation command's reply survives a fence move — use-structured-agent-session-mutate.ts keeps the reply the pane still waits on, so a /clear or /compact on an at-rest chat no longer loses its own answer; structured-conversation-command-send.ts words the failure from the fact and stays silent when the chat's loaded start-failure row already states it.
  • History pane says it once — NativeChatEmptyState + structured-agent-session-read-failure-notice.ts add headlineSaysUnread/saysUnread so the line under a title that already says the history didn't load only adds that Orca keeps trying.
  • i18n — 57 keys added across en/es/fr/ja/ko/zh, conversationCommand.unconfirmed removed, keepsTrying added; locale coverage verified complete (59 real pieces = 56 failureWords + 3 shared writeNotice).

ℹ️ resolution nested keys evade the whole-fact equality

readWholeAgentSessionFailureFact relies on structural equality to prove the fact was read in full, but readAgentSessionRefusalDetails keeps resolution by reference ({ resolution: value }), and the equality helper short-circuits on equal references before walking nested records. Unknown keys nested inside resolution are therefore never detected. No current sentence reads resolution, so this is latent rather than a shipped bug — flagging only because it weakens the "read all of it" guarantee the PR's copy logic rests on.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

enabledRef.current = enabled
}, [enabled])
commandInFlight.current = null
}, [enabled, sessionId])

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reset effect keys only on [enabled, sessionId], so commandInFlight survives a host change even though target is not constant for a mounted pane: NativeChatView keys the pane by sessionId, while StructuredAgentSessionPaneOverlayLayer re-derives target from the worktree's runtime owner. A command in flight when the owner changes is then kept via the waitedOn branch of settlesHere and applied to the pane now bound to the new host, showing/suppressing a composer error or clearing the draft for the wrong runtime. The sibling use-structured-agent-session-outbox.ts resets its in-flight refs on [owner.ownerChange, owner.targetKey, sessionId]; mirror that here.

Technical details
# Reset the conversation-command exception when the runtime owner changes

## Affected sites
- src/renderer/src/components/native-chat/use-structured-agent-session-mutate.ts:68-72 — the reset effect omits `target`/owner, unlike `write`'s dependency array `[enabled, sessionId, stateRef, target]`
- src/renderer/src/components/native-chat/use-structured-agent-session-outbox.ts:96-100 — the established reset, on `owner.ownerChange`, `owner.targetKey`, `sessionId`
- src/renderer/src/components/native-chat/StructuredAgentSessionPaneOverlayLayer.tsx:72-75,107 — `target` varies per `runtimeEnvironmentId` while `sessionId` (`tab.entityId`) stays fixed; `NativeChatView.tsx:15` keys only by `sessionId`

## Required outcome
- A reply kept via `waitedOn` must be dropped when the pane's host/runtime target changed while the command was in flight, so `commandInFlight` is cleared on that change just as a session change clears it.

## Suggested approach (optional)
- Add the host/owner key (e.g. the same `structuredAgentSessionHostKey(target)` / owner change signal the outbox hook uses) to the effect's dependency list, rather than `target` object identity alone.

## Open questions for the human (optional)
- Is a host change with the same `sessionId` expected to keep the pane mounted, or does some upstream layer remount it? The overlay layer suggests the former.

…st's own words

A newer host can answer a conversation command with a command name this build doesn't know. This
build re-worded that reply from its failure fact as if it were a command it knew, naming the
command in its own words, or said nothing when a loaded start row matched the fact. It now shows
the host's sentence as written, as it already does for a fact it can read only in part.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 01da184f-3e45-4dea-ad73-9800025c2fcd

📥 Commits

Reviewing files that changed from the base of the PR and between 7afa4ee and 63caa6e.

📒 Files selected for processing (41)
  • mobile/src/session/mobile-structured-queued-message-cards.test.ts
  • mobile/src/session/mobile-structured-queued-message-cards.ts
  • src/renderer/src/components/native-chat/NativeChatEmptyState.tsx
  • src/renderer/src/components/native-chat/NativeChatLaunchRetry.tsx
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageCard.tsx
  • src/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.read-error.test.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
  • src/renderer/src/components/native-chat/agent-session-failure-words-text.test.ts
  • src/renderer/src/components/native-chat/agent-session-failure-words-text.ts
  • src/renderer/src/components/native-chat/agent-session-write-notice-text.test.ts
  • src/renderer/src/components/native-chat/agent-session-write-notice-text.ts
  • src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.test.ts
  • src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.ts
  • src/renderer/src/components/native-chat/structured-agent-session-read-failure-notice.ts
  • src/renderer/src/components/native-chat/structured-conversation-command-send.test.ts
  • src/renderer/src/components/native-chat/structured-conversation-command-send.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-command-start-failure.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session-mutate.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-transport.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-write-refusal.test.tsx
  • src/renderer/src/components/native-chat/use-structured-agent-session.ts
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/shared/agent-session-conversation-command.ts
  • src/shared/agent-session-failure-copy.ts
  • src/shared/agent-session-failure-words.ts
  • src/shared/agent-session-failure.test.ts
  • src/shared/agent-session-failure.ts
  • src/shared/agent-session-refusal-notice.test.ts
  • src/shared/agent-session-refusal-notice.ts
  • src/shared/agent-session-write-notice-copy.ts
  • src/shared/native-chat-empty-state.ts
  • src/shared/sentence-joining.ts
  • src/shared/structured-agent-session-outbox.ts
  • src/shared/structured-agent-session-send-disposition.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review.

Comment on lines +55 to +75
) {
return { accepted: false, error: null }
}
const error = conversationCommandFailureText(value, input.agentName)
return { accepted: value.state === 'completed' && !error, error }
} finally {
input.pending.current = false
}
}

/** The host's sentence in the reader's language, from the fact beside it; with no fact (an older
* host), one this build can't read whole, or a command it doesn't know, the sentence as written. */
function conversationCommandFailureText(
result: AgentSessionConversationCommandResult,
agentName: string
): string | null {
const fact = isAgentSessionConversationCommand(result.command)
? readWholeAgentSessionFailureFact(result.failure)
: undefined
if (!fact) {
return result.error ?? null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

sed -n '1,120p' src/renderer/src/components/native-chat/structured-conversation-command-send.ts
sed -n '35,125p' src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.ts
sed -n '115,175p' src/renderer/src/components/native-chat/structured-conversation-command-send.test.ts

Repository: stablyai/orca

Length of output: 9833


🏁 Script executed:

set -o pipefail
printf '%s\n' '--- failure module outline ---'
ast-grep outline src/shared/agent-session-failure.ts
printf '%s\n' '--- failure parser definitions/usages ---'
rg -n -A45 -B10 'readWholeAgentSessionFailureFact|readAgentSessionFailureFact|sameAgentSessionFailureFact|agentSessionFailureStatedByStartRow' src/shared src/renderer/src/components/native-chat
printf '%s\n' '--- focused tests around command handling and suppression ---'
rg -n -A35 -B20 'StatedByStartRow|startFailures|START_FACTS|START_FAILED|compact|duplicate|rejected' src/renderer/src/components/native-chat/structured-conversation-command-send.test.ts src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.test.ts

Repository: stablyai/orca

Length of output: 42799


🏁 Script executed:

set -o pipefail
ast-grep outline src/shared/agent-session-failure.ts
rg -n -A45 -B10 'readWholeAgentSessionFailureFact|readAgentSessionFailureFact|sameAgentSessionFailureFact|agentSessionFailureStatedByStartRow' src/shared src/renderer/src/components/native-chat
rg -n -A35 -B20 'StatedByStartRow|startFailures|START_FACTS|START_FAILED|compact|duplicate|rejected' src/renderer/src/components/native-chat/structured-conversation-command-send.test.ts src/renderer/src/components/native-chat/structured-agent-session-delivery-notices.test.ts

Repository: stablyai/orca

Length of output: 42317


🏁 Script executed:

sed -n '1,260p' src/shared/agent-session-failure.ts
printf '%s\n' '--- command-send tests ---'
sed -n '1,260p' src/renderer/src/components/native-chat/structured-conversation-command-send.test.ts
printf '%s\n' '--- matcher and callers ---'
rg -n -A25 -B15 'agentSessionFailureStatedByStartRow|sendStructuredConversationCommand|structuredAgentSessionStartFailureFacts' src/renderer/src/components/native-chat

Repository: stablyai/orca

Length of output: 42276


🏁 Script executed:

rg -n -A20 -B20 'sendStructuredConversationCommand\(' src/renderer/src --glob '!*.test.ts' --glob '!*.spec.ts'
rg -n -A20 -B12 'useStructuredAgentSessionStartFailureFacts|startFailures' src/renderer/src/components/native-chat --glob '!*.test.ts' --glob '!*.spec.ts'

Repository: stablyai/orca

Length of output: 42106


Require a complete failure fact before suppressing the command error.

The production caller passes the loaded start-failure rows to sendStructuredConversationCommand. For a compact result with a known kind and an unsupported extra field, readAgentSessionFailureFact drops that field. The partial result can match a loaded row containing only the same kind.

The match returns { accepted: false, error: null } before conversationCommandFailureText can preserve result.error. Use the whole-fact reader for duplicate suppression.

Suggested fix
-  const fact = readAgentSessionFailureFact(failure)
+  const fact = readWholeAgentSessionFailureFact(failure)

"subtitle": "No se pudo leer la transcripción. Vuelva a la terminal para seguir trabajando.",
"retryingSubtitle": "No se pudo leer la transcripción. Orca sigue intentando cargarla."
"retryingSubtitle": "No se pudo leer la transcripción. Orca sigue intentando cargarla.",
"keepsTrying": "Orca sigue intentando cargarlo."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
set -eu
printf '%s\n' '--- Spanish and French matching keys ---'
rg -n -C 8 '"(keepsTrying|retryingSubtitle)"' src/renderer/src/i18n/locales/es.json src/renderer/src/i18n/locales/fr.json
printf '%s\n' '--- Nearby Spanish locale section ---'
sed -n '15070,15110p' src/renderer/src/i18n/locales/es.json
printf '%s\n' '--- Nearby French locale section ---'
rg -n -C 20 '"retryingSubtitle"' src/renderer/src/i18n/locales/fr.json
printf '%s\n' '--- Relevant keys/usages ---'
rg -n -C 4 'retryingSubtitle|keepsTrying|history.*(headline|title)|conversation.*(headline|title)' src/renderer/src
printf '%s\n' '--- PR diff for the affected locale files ---'
git diff --unified=8 7afa4ee3dc69db4e7df6f8669c503261bf76c50e 63caa6e1abcabeb844a21d1cea152b2fedc9b167 -- src/renderer/src/i18n/locales/es.json src/renderer/src/i18n/locales/fr.json

Repository: stablyai/orca

Length of output: 38820


🤖 get_repo_knowledge executed:

get_repo_knowledge stablyai/orca /tmp/coderabbit-repo-knowledge/stablyai-orca-89dc44e4

Length of output: 3853


Use the feminine pronoun in keepsTrying.

The Spanish error title refers to “la conversación,” and retryingSubtitle refers to “la transcripción.” Change cargarlo to cargarla. Make the equivalent French change from de le charger to de la charger.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta. The one previously flagged concern on use-structured-agent-session-mutate.ts:72 (the commandInFlight reset not keying on the runtime owner) is untouched by this commit and remains open.

Reviewed changes

  • An unrecognised conversation command falls back to the host's sentence — isAgentSessionConversationCommand (src/shared/agent-session-conversation-command.ts) now guards structured-conversation-command-send.ts: a reply whose command is neither clear nor compact skips the start-row suppression and returns the host's error verbatim, instead of trying to word a sentence for a command this build cannot place. isAgentSessionConversationCommandResult reuses the same guard.
  • Coverage — a new case drives a {"command":"rewind"} reply through sendStructuredConversationCommand and asserts the host's English sentence is shown unchanged; without the guard the loaded start-failure row would have suppressed it, so the test can fail.

Pullfrog  | Fix it ➔ | View workflow run | Using DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025
brennanb2025 merged commit afa81dc into main Sep 30, 2026
35 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant