Repository navigation
fix(native-chat): chat failure messages appear in the app's language - #23674
Conversation
…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 locale, which a refusal notice now reads
…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).
…lure-words-finish
…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.
…lure-words-finish
…lure-words-finish
|
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
Deferred (P3 or already on main)
Verified
Not verified
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. |
|
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 configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (6)
🚧 Files skipped from review as they are similar to previous changes (5)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 6 remain after this review. 📝 WalkthroughWalkthroughThe 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 The pending-work notice uses the configured English fallback when Spanish copy is absent; no material regression in the reviewed changes remains. Architecture SummaryArchitecture risk: 🔵 Low · up to The change affects 2 systems. Changed systems: Architecture concerns Review detailsSystems and components
Before / after behavior
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Docstring CoverageExplanation 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.)
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. Comment |
|
| if ( | ||
| value.command !== 'clear' && | ||
| agentSessionFailureStatedByStartRow(value.failure, input.startFailures()) | ||
| ) { | ||
| return { accepted: false, error: null } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
ℹ️ 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.tsholds 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 —
readWholeAgentSessionFailureFactreturns a fact only whenstructuralValuesEqualIgnoringUndefinedproves this build read all of it; used by the mobile caption,structuredAgentSessionRejectedFailure, desktop delivery notices, and command replies. - Language-aware joining —
sentence-joining.tsdrops 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.tskeeps the reply the pane still waits on, so a/clearor/compacton an at-rest chat no longer loses its own answer;structured-conversation-command-send.tswords 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.tsaddheadlineSaysUnread/saysUnreadso 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.unconfirmedremoved,keepsTryingadded; locale coverage verified complete (59 real pieces = 56failureWords+ 3 sharedwriteNotice).
ℹ️ 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.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
| enabledRef.current = enabled | ||
| }, [enabled]) | ||
| commandInFlight.current = null | ||
| }, [enabled, sessionId]) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 01da184f-3e45-4dea-ad73-9800025c2fcd
📒 Files selected for processing (41)
mobile/src/session/mobile-structured-queued-message-cards.test.tsmobile/src/session/mobile-structured-queued-message-cards.tssrc/renderer/src/components/native-chat/NativeChatEmptyState.tsxsrc/renderer/src/components/native-chat/NativeChatLaunchRetry.tsxsrc/renderer/src/components/native-chat/NativeChatQueuedMessageCard.tsxsrc/renderer/src/components/native-chat/NativeChatQueuedMessageList.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.read-error.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.tsxsrc/renderer/src/components/native-chat/agent-session-failure-words-text.test.tssrc/renderer/src/components/native-chat/agent-session-failure-words-text.tssrc/renderer/src/components/native-chat/agent-session-write-notice-text.test.tssrc/renderer/src/components/native-chat/agent-session-write-notice-text.tssrc/renderer/src/components/native-chat/structured-agent-session-delivery-notices.test.tssrc/renderer/src/components/native-chat/structured-agent-session-delivery-notices.tssrc/renderer/src/components/native-chat/structured-agent-session-read-failure-notice.tssrc/renderer/src/components/native-chat/structured-conversation-command-send.test.tssrc/renderer/src/components/native-chat/structured-conversation-command-send.tssrc/renderer/src/components/native-chat/use-structured-agent-session-command-start-failure.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session-mutate.tssrc/renderer/src/components/native-chat/use-structured-agent-session-transport.tssrc/renderer/src/components/native-chat/use-structured-agent-session-write-refusal.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.tssrc/renderer/src/i18n/en-runtime-required.jsonsrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/shared/agent-session-conversation-command.tssrc/shared/agent-session-failure-copy.tssrc/shared/agent-session-failure-words.tssrc/shared/agent-session-failure.test.tssrc/shared/agent-session-failure.tssrc/shared/agent-session-refusal-notice.test.tssrc/shared/agent-session-refusal-notice.tssrc/shared/agent-session-write-notice-copy.tssrc/shared/native-chat-empty-state.tssrc/shared/sentence-joining.tssrc/shared/structured-agent-session-outbox.tssrc/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.
| ) { | ||
| 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 |
There was a problem hiding this comment.
🎯 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.tsRepository: 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.tsRepository: 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.tsRepository: 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-chatRepository: 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." |
There was a problem hiding this comment.
🎯 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.jsonRepository: 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.
There was a problem hiding this comment.
ℹ️ No new issues in this delta. The one previously flagged concern on
use-structured-agent-session-mutate.ts:72(thecommandInFlightreset 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 guardsstructured-conversation-command-send.ts: a reply whosecommandis neitherclearnorcompactskips the start-row suppression and returns the host'serrorverbatim, instead of trying to word a sentence for a command this build cannot place.isAgentSessionConversationCommandResultreuses the same guard. - Coverage — a new case drives a
{"command":"rewind"}reply throughsendStructuredConversationCommandand 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.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…lure-words-finish

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
What Changed
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).readWholeAgentSessionFailureFactdecides 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.joinSentencesputs no space after 。 in Japanese and Chinese, and a space elsewhere.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.Why
Differences from the common pattern
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:
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:
/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:
/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:
A queued message returned unsent, captioned in French. The queue was switched on locally for this capture; it is off on
main:Testing
mainand this branch.Known weaknesses
main.conversationCommand.pendingWork). That was already so onmain.Needs native review
The new
components.native-chat.failureWords.*keys andcomponents.native-chat.state.error.keepsTrying, hand-translated into Spanish (es), French (fr), Japanese (ja), Korean (ko) and Chinese (zh).Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.