Files
orca/tests/e2e/cross-version-wire
Brennan Benson 98171a8934 fix(native-chat): never delete chat history on read; a chat Orca can't load says why once (#24576)
* fix(native-chat): an older Orca keeps a chat with newer content read-only, and an unreadable annotation costs only itself

A body or lifecycle mutation of a kind this build does not know, inside a row of a known kind,
now latches the chat read-only with every row kept, as a newer row kind or version already does.
It used to read as damage, and the open deleted the journal from that row on.

Each optional field of a body schema now declares what an unparseable value means:
- droppable (an annotation): removed from the row in memory; the chat stays writable;
- must-understand (a prompt's questions, a plan approval's plan, a turn's lifecycle, a goal
  change): the row is unreadable and the chat latches read-only.
Only a required field that fails is damage, repaired as before. A test holds every optional
field reachable from a body schema to a policy; the known body kinds are read off the schema.

Prompt options and questions no longer reject a key this build does not know, which deleted the
journal from that row on. The context-usage drop is now one case of the droppable rule.

* feat(native-chat): a chat an older Orca keeps read-only says so before a send is refused

A chat a newer Orca wrote opens read-only, and until now nothing said so: the person found out
when a send failed with "Chats were saved by a newer Orca". The host now names the reason on
every whole history page it serves (`page.readOnly: 'written-by-newer-orca'`: hydration, history
and catch-up resets), the shared client reducer keeps it from the latest whole page, and the
desktop status strip and the phone's composer area say "This chat was saved by a newer Orca, so
it's read-only here. Update Orca to continue this chat."

Every read-only latch the host has is a newer Orca's (its database, a row version, a row kind, a
body kind or must-understand fact), so the reason is derived from the journal's latch, not stored.
The field is optional: older clients ignore it (cross-version test against the newest release's
reducer), and older hosts never send it, so a new client against one says nothing, as today. A
reason this client does not know shows nothing rather than words that may be wrong.

The window-merge helpers move out of the client reducer into their own module.

* fix(native-chat): only a newer build's value goes read-only; damage repairs as before

An older Orca keeps a chat read-only only on evidence a newer build wrote it: a value outside a
closed set this build knows (a body kind, a nested discriminant or literal, a mutation kind), read
off zod's issues. That evidence wins over damage beside it. Anything else that fails (a wrong type,
a missing field, a bad optional value) is damage, repaired as before, so damage no longer freezes
a chat behind an "update Orca" it cannot fix. Drops the per-field droppable/must-understand
policy. A lifecycle mutation's kind is decided before its item id, so a newer kind without one is
kept. The context-usage drop is unchanged from main.

* fix(native-chat): a read-only chat locks its composer and says why there

Removes the separate read-only line (desktop status strip, phone notice component). The host's
`readOnly` on whole pages now feeds the existing composer lock instead: desktop disables the
composer with "Saved by a newer Orca. Update Orca to continue this chat." as its placeholder, and
the phone adds a 'read-only' input-lock reason with the same words. A read-only chat shows no
prompt card it would refuse; the draft stays in the composer. The phone's send-failure line is
back exactly as on main.

* fix(native-chat): a read-only chat pages back through its history

A read-only journal answered every history request with a reset, so a chat this PR now keeps
read-only more often could show only its newest page. Backward reads now serve the snapshot the
open folded, as the first page already does; a forward read of rows still resets.

* test(native-chat): an unknown key in a question entry is read and kept

* test(mobile): the read-only overlay test typechecks

* chore(native-chat): satisfy the changed-code gate in admission and the read-only overlay test

* fix(native-chat): a new value inside a known block or goal arm reads as a newer build's

The open fallback arm of the block and goal unions now aborts, so zod reports the known arm's own
failure instead of only the fallback's; a future closed set there reads unreadable, damage there
still repairs. The schema headers name the context-usage exception and say a new open-string
value must be safe for every older build (rewind keeps only known states; pages carry no row
version).

* fix(native-chat): a read-only chat reconnects for a whole page, so an updated host unlocks it

The client keeps `readOnly` until a whole page replaces it, but reconnected at its cursor and got
only batches, so a desktop attached to a host updated in place stayed locked with "Update Orca".
While it holds the latch it now subscribes without a cursor; the snapshot re-derives the state on
any host version. The phone already subscribes without one.

* fix(native-chat): every desktop write control follows the one read-only fact

The structured session's transport state derives one fact from the page's `readOnly`: the words
for why the host refuses writes. Every write control reads it. While it holds, the chat has no
turn and no work, as the host projects it, so the working row and Stop go away; a pending prompt
stays shown above the composer with its card disabled; queued cards' Send now, Delete and Edit,
the queue's Resume, the goal banner's actions, the model and option pickers, dictation and
background-task Stop are disabled or withheld. The composer placeholder stays the one place the
reason is said. `lockReason` now locks the composer by itself.

* fix(mobile): every phone write control follows the one read-only fact

The phone session derives one fact from the page's `readOnly`, the words for why the host refuses
writes, and every write control reads it: no turn or work while it holds, so no Stop; the pending
prompt stays shown with its card disabled; queued cards and Resume are inert; the model and option
pickers stay shut; the composer field is not editable, so its placeholder keeps saying why, and the
lock applies at once instead of after the transport lock's settle.

* test(native-chat): the newest release hydrates a read-only chat and scrolls back through it

* test(native-chat): a read-only chat reads idle even with a send its provider never answered

* test(native-chat): composer lock test without type assertions

* fix(native-chat): round-3 read-only fixes

- An old /model or option request no longer reopens its picker when a read-only chat unlocks: the
  menu is keyed by the request alone, and a lock only keeps it from mounting open.
- A disabled question card still steps through its questions; only answering is locked.
- A failed send's notice in a read-only chat says only that the message was not sent, with no
  Retry: the composer already says why, and no retry can land. Its wiring moves into
  useStructuredAgentSessionDeliveryNotices.
- The host's background-task stop flags stand; the client no longer overrides them.
- The view reads the read-only fact through one local flag.
- The schema header says what an older build's rewind does with each open string.

* test(native-chat): count picker mounts without an effect

* fix(native-chat): read-only keeps other failures' words and shuts open option menus

- In a read-only chat, only a send the newer-Orca refusal stopped is shortened to "Your message
  was not sent."; any other saved failure keeps its own words, and none offers a Retry.
- A model or option menu open when the lock lands has its items disabled, so it cannot send a
  change the host would refuse.

* test(native-chat): lock-lands picker test uses a typed surface

* fix(native-chat): a locked composer draws its reason

The editor's placeholder used the default that draws nothing while the editor is not editable, so
a read-only chat's composer was empty and its reason lived only in the aria-label. The placeholder
now draws while disabled, and its words are empty unless the composer is locked with a reason, so
every other disabled composer (a pending prompt, no terminal) looks as before.

* fix(native-chat): sync the locked-placeholder flag in an effect, not during render

* fix(native-chat): a read-only chat is not offered for resume or counted as failed to resume

After a relaunch, a chat whose journal this build keeps read-only (a newer Orca's) was listed in
"Resume interrupted chats?", failed with "Orca couldn't resume this chat. Open it to continue
manually.", and left a status-bar "N chats failed to resume" that never cleared, since nothing can
continue a chat this build cannot write. The resume set now skips such a chat after the checks
that end an offer (fork, moved on), so the offer is kept, not spent, and an updated Orca offers it
again; the failure ledger keeps an already-filed failure for it but does not show it. Both read
the chat's open journal, which listing and acting open first.

* fix(native-chat): a refused resume continuation keeps the refusal's reason

A send refused while continuing a chat after a restart was filed with its code alone, so a newer
Orca's refusal lost its `journalWrittenByNewerOrca` reason. The continuation now carries the whole
refusal, as a refusal from the agent's start already did, so the filed failure keeps it.

* fix(native-chat): a newer Orca's chat keeps its resume offer however it is met

- A newer Orca's whole database counts as read-only for resume, so a chat whose journal cannot
  be opened at all (a table the newer schema changed) is not offered, run or counted either.
- The checks made right before sending no longer skip read-only chats: turning one away there was
  read as the user having moved on and deleted the offer. Its send is refused instead.
- Settling a resume files nothing for a newer Orca's refusal; the existing rollback reopens the
  offer for an updated Orca.
- The continuation records the refusal as a reference, without the wire prose.

* chore(native-chat): one import of the session wire types in the continuation

* docs(native-chat): resume and read-only comments match the current rule

* test(native-chat): a closed set of a journal row cannot change without the row version

The test walks the body schema and the row and mutation kind tables, collects every closed set (a
row kind, a mutation kind, each enum, literal or discriminant), and compares them with a snapshot
recorded beside the row version. A new value makes older builds go read-only, so it fails until
the reader ships first or `v` is bumped and the snapshot updated. Tags a catch-all arm accepts as
any string (block types, goal states) are listed apart: older builds read a new one as-is.

* feat(native-chat): a body or plan subject of a newer kind is kept and the chat stays writable

An item body kind and an approval subject kind this build does not know now read through the
same catch-all blocks and goal states use (`openDiscriminatedUnion`), instead of making the chat
read-only. The item is kept, drawn by nobody, and ignored by everything that reads items (turns,
prompts, status); a rewind carries it as it was, kept by its place like every other row. An
approval whose subject this build cannot draw shows its `detail`, which Orca's writers fill with
the subject's text. A sent message of a kind this build does not know stays a newer build's.

* fix(native-chat): rewind narrows a retained body by its kind before normalizing it

* fix(native-chat): one chat that cannot open no longer stops the startup restore of the rest

* fix(native-chat): a damaged chat fails to load and nothing is deleted

A row this build cannot parse, a gap, or an epoch without its first row used to
be repaired on open: every row from the damage on was deleted and a rebuild from
the provider transcript was attempted. The open now fails through one refusal
(failLoadOnJournalDamage, journalCorrupt), every row stays, and a damaged
per-chat file is kept whole and never copied. A newer build's row still wins
and keeps the chat read-only. The body classifier for closed-set values, the
repair marker and disclosure, and journal recovery from the transcript go.

* chore(native-chat): no reset or adapter left on the open's fixtures

* feat(native-chat): an approval of a newer Orca's subject kind can only be cancelled

Desktop and phone show its detail and the line "This request needs a newer
version of Orca.", disable every answer, and keep the card's cancel, which ends
the turn. The chat stays writable.

* test(native-chat): the closed-set guard says a missed version bump fails older builds' load

* test(native-chat): the closed-set guard names what a missed version bump does

* fix(native-chat): a row the reader rejects is never written, and a chat that cannot load keeps its tab

Every journal insert reads its serialized row back with the reader inside the
write's transaction and refuses one it rejects, so Orca's own writer can no
longer leave a chat that fails to load. A restored chat whose open fails keeps
its tab and says why when opened. An epoch named with no rows is founded afresh
again, deleting nothing. The at-rest test now expects a damaged chat to be
refused; the replay gate keeps its 2026-09-11 evidence as it was run.

* fix(native-chat): a newer Orca's approval subject is carried as it was, and its card cancels with or without a turn

The approval subject's type is open, so code that reads a plan narrows first; the
host's prompt bounding carries an unknown subject instead of rebuilding it as a
plan, which threw in every start's stale settlement. A Codex rewind keeps a
newer Orca's item in its place. A whitespace-only tag is damage. The card's
cancel reaches the host without a running turn on hosts that take that. A
provider row the reader rejects ends its turn as interrupted and the next send
works. A guard fails if an approval subject kind is added before clients can be
gated (STA-9262).

* fix(native-chat): a card's cancel sends nothing without a turn again, and rewind keeps a newer row after any held row

The host's cancel request has to name a turn on every version, so the turnless
prompt cancel is reverted on desktop and phone. The rewind merge moves its anchor
for every row the provider still holds, so a newer Orca's row stays after one
whose kind this build does not know.

* fix(native-chat): a card this build cannot answer leaves the composer open, and a send starts a turn

An approval of a subject kind this build cannot draw, raised by a newer host's background agent
while no turn runs, left nothing to press: its answers are disabled, its cancel needs a turn, and
the desktop composer gave the card its slot. A send the host queues waits behind any pending
prompt, so even the phone's open composer only queued.

While every pending prompt is one this build cannot answer, the desktop composer stays open, and
desktop and phone send without asking to queue: the send starts a turn, and the card's cancel then
settles it. A chat a dead run left is unaffected: opening it already cancels those prompts.

* feat(native-chat): a chat a newer Orca saved fails to load and says to update, with nothing to send into

Opening a chat a newer Orca saved (a row kind, batch-change kind or sent-message kind this build does
not know, a newer row version, or a newer history store) used to open it read-only: the history
shown, the composer locked with a reason, every control greyed. Now it fails the load, like a
damaged chat, with its own words: "This chat was saved by a newer Orca. Update Orca to open it."
Every row is kept. The read is final, so the pane stops retrying; reopening the tab reads again.

A chat whose load failed for good (damaged, or a newer Orca's) offers no composer under the error,
on desktop and on the phone, so the failure is said once and no send can be refused a second time.

A restart offer for a newer Orca's chat is spent without a word: no failure filed, nothing counted.

Removed with the read-only mode: the journal's read-only latch and every check of it, the history
page's readOnly field (never released), the cursorless reconnect, read-only paging, the composer
lock and placeholder, the greyed controls, and the resume exclusion.

* test(native-chat): a newer Orca's records open no chat, and a status fake carries its submissions

* test(native-chat): the status fake's cast says what the feed reads

* fix(native-chat): a chat's read that failed for good takes the whole pane, over a loaded transcript too

* test(native-chat): the final-read-failure test names its reasons

* fix(native-chat): a newer Orca's chat keeps its restart offer, and its refused load is logged once

* refactor(native-chat): the newer-Orca chats a restart listing skips live beside the candidate reader

* fix(native-chat): a refused chat load keeps where it failed, so the log names it and a new reason logs again

* chore(native-chat): the reducer's window merge back where main has it, and comments say a newer Orca's chat fails to load

* test(agent-hooks): the rename test's journal fakes carry submissions, as a journal snapshot does

* test(agent-hooks): the rename test's journal fakes say what the status feed reads

* fix(native-chat): Dismiss all ends the restart offers this Orca lists, and keeps a newer Orca's hidden ones

* test(native-chat): a resend against a newer Orca's store answers unknown, and the phone says nothing beside the read's words

This build never opens a chat a newer Orca's database holds, so a resent send id there cannot be
answered from its journal: it answers "outcome unknown", never a refusal and never a made-up
record, with no second delivery and no write. On the phone, a send error left beside a read that
failed for good is not shown; the pane keeps only the read's words.
2026-10-05 10:45:58 -07:00
..