Skip to content

fix(native-chat): never delete chat history on read; a chat Orca can't load says why once - #24576

Merged
brennanb2025 merged 57 commits into
mainfrom
brennanb2025/journal-unknown-content-read-only
Oct 5, 2026
Merged

brennanb2025 merged 57 commits into
mainfrom
brennanb2025/journal-unknown-content-read-only

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 75 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​3073 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1287 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1786
Prod 94 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​1350 $\color{#cf222e}{\Huge{\mathbf{−}}}$​1248 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​102

ELI5

Every native chat keeps its history as a numbered list of rows. Each row holds one piece of the conversation (a message, a tool call, a question, a turn's start or end).

Two things made Orca throw that history away when it opened a chat:

  1. Content a newer Orca wrote. When a newer Orca saves a chat and you open it in an older Orca (after a downgrade, or on a second machine that has not updated yet), the older Orca can meet something it has never seen. A new value inside a known row (a new kind of message content, a new kind of thing a plan approval asks about) was called damage, and Orca deleted that row and every row after it.
  2. Damage. A row Orca could not read at all (not valid, a missing field, a gap in the numbering) was "repaired" by deleting it and every row after it, then rebuilding what it could from the agent's own transcript. Sends, Stops and Resumes that only Orca recorded were gone for good.

After this change, opening a chat never deletes anything:

  • A new kind of message content or plan subject from a newer Orca is kept, and the chat stays usable. This build draws nothing for content it does not know. An approval asking about something it cannot draw shows the text it has and "This request needs a newer version of Orca."; it cannot be approved here, only cancelled, and the message box stays usable beside it.
  • A chat a newer Orca saved in a way this build cannot place (a new kind of row, of change or of sent message, a newer row format, or a newer history store) does not open. It says "This chat was saved by a newer Orca. Update Orca to open it." Nothing is deleted, so it opens whole once Orca is updated.
  • A damaged chat says "Unable to load this chat." Nothing is deleted. You start a new chat to carry on.
  • A chat that cannot open says why once: its whole pane shows only that, even if the chat was already on screen when it happened.

What Changed

What you see

Before, opening a chat:

  • In an older Orca, a row holding a value it did not know (a new kind of message content, of change in a batch, of sent message, of plan subject) showed "1 journal line could not be read". Orca deleted that row and every later one and rebuilt what it could from the agent's transcript.
  • A damaged row did the same: a line saying part of the chat's history couldn't be loaded, everything after it deleted, and the chat stayed usable on what was left.
  • A chat with a newer row format, row kind or history store opened read-only but looked writable: the first sign was a failed send with a Retry that always failed, and every other control (Stop, queued messages, the goal, the pickers) stayed live and failed when pressed.
  • At startup, one chat that could not be opened stopped every other chat's tab from being restored.

After:

  • New content inside a known row stays usable. A message of a kind this build does not know is kept and not drawn; the chat stays writable.
  • An approval this build cannot fully show cannot be approved. When its subject is of a kind this build does not know, the card (desktop and phone) shows the approval's text if it has any, then "This request needs a newer version of Orca." Every answer button is greyed out and only the card's cancel works: on Claude it turns the request down and the turn goes on, on Codex it ends the turn. Desktop translates the line; the phone shows it in English.
  • An approval this build cannot answer never leaves the chat stuck. A card's cancel needs a running turn, and a newer Orca's background agent can raise such an approval while no turn runs. While every pending request is one this build cannot answer, the message box stays open beside the card on desktop (the phone already kept it open), and a message you send goes to the agent at once instead of waiting in the queue behind the card. That starts a turn, and the card's cancel then works. A request left behind by an agent run that has ended is cancelled when the chat opens, as before.
  • A newer Orca's pending approval no longer stops the chat from starting its agent. Before, the host rebuilt every approval subject as a plan when it settled a request its previous run left behind; a subject of another kind made that throw on every start. It is now kept exactly as written.
  • A chat a newer Orca saved does not open, and says so once. On desktop and on the phone the whole chat pane shows "This chat was saved by a newer Orca. Update Orca to open it." and nothing else: no messages, no message box, no Stop, no cards, no queued messages, no goal, no task strip, no "trying again". That holds also when it happens while the chat is open, for example when the Orca running it restarts as an older build and the chat reconnects. Every row stays as it was. Reopening the tab reads it again, so once Orca is updated the chat opens whole (the phone also reads again when it reconnects). This covers a new kind of row, of change or of sent message, a newer row format, and a newer history store (every chat in it). Desktop translates the two sentences; the phone shows them in English.
  • A damaged chat fails to load, keeps every row, and says so once. Its whole pane shows "Unable to load this chat." and nothing else, also when the damage is found while the chat is open (before, the transcript and every control stayed, and a send was refused with the same failure again). The rows stay exactly as they were, including a per-chat history file from an older build, which is kept whole and not copied in. You start a new chat to carry on. The realistic way a row goes bad is Orca's writer and reader drifting apart, not a half-written row, which SQLite transactions do not leave; so every row is now read back with the reader before it is written, and a row the reader would reject is refused at write time through that operation's existing failure (for a provider's output, the turn stops with an internal error, as for any failed journal write).
  • One chat that cannot load no longer blocks the rest at startup. Its failure is logged and the other chats restore. It keeps its tab, including on a phone attached to a headless or remote Orca, and opening it shows why.
  • After a restart, a chat a newer Orca saved is not offered to continue here, and nothing is counted. Its offer is kept as it was: the newer Orca that saved it can still offer it, and so can this Orca once it is updated. A failure that newer Orca filed for it is not shown here either. "Dismiss all" ends the offers and failures this Orca shows and leaves these hidden ones as they were, since the person never saw them.
  • A chat an older Orca repaired and never rebuilt shows the history from before the damage, as that build left it, with no notice beyond the line it may already hold saying part of its history couldn't be loaded. This build does not try the rebuild again.
  • A chat left with an empty history by a crash in the middle of an older build's repair opens as a fresh chat, as before; nothing is deleted.

Mechanism

  • What a newer Orca may add (src/shared/agent-session-journal-schemas.ts). A body's kind, a block's type, a goal's state and an approval subject's kind are open: one shared helper (agent-session-journal-open-union.ts) keeps any tag this build does not know, as long as it has something other than whitespace in it (an empty or whitespace-only tag is damage). Everything else extends only by new keys or a bumped row version: a new value in any other fixed list of allowed values requires a row-version bump, and journal-closed-sets.test.ts fails if one changes without it. journal-approval-subject-kinds.test.ts fails if an approval subject kind is added at all before clients can be told apart (STA-9262).
  • A chat a newer Orca saved fails its load (journal-open.ts, failLoadOnUnloadableJournal in journal-open-failure.ts). Replay names the first row this build cannot place (a row kind, batch-change kind or sent-message kind it does not know, or a newer row version) and the open is refused with the existing agent_session_journal_unreadable / journalWrittenByNewerOrca reason. A newer row wins over damage beside it, since an update is what gets past both. The same function refuses at every place damage is refused: the store's open, a deferred per-chat copy, and the startup preview of a per-chat file (a newer one is never copied in). A newer history store refuses every chat at restore (journal-store-restore.ts); its chat records stay read-only, as before.
  • Its words (agent-session-refusal-reason-words.ts, the reason table moved out of agent-session-refusal-notice.ts): when what failed is reading a chat's history, the reason reads "This chat was saved by a newer Orca. Update Orca to open it." A write that meets it (creating a chat on a newer history store, or a send to an older Orca that keeps such a chat read-only) keeps "Chats were saved by a newer Orca. Update Orca to keep using them." isFinalAgentSessionReadRefusal counts it as final, as damage is, so the pane stops retrying.
  • A read that failed for good is a view state: it takes the whole pane. Desktop: NativeChatStructuredSession.tsx lets a final read failure win over messages in selectNativeChatViewState (as a read that does not retry already did), and renders nothing below the pane while it holds: no launch retry, queued cards, status and task strip, goal, cards or composer. Phone: the read failure now carries its refusal into the shared reducer (use-mobile-structured-agent-state.ts), the session reports readFailedFinally, and MobileNativeChatView.tsx renders only the existing failed-load surface. A failure that can still clear keeps today's view.
  • Every row is checked before it is written (insertJournalRow in journal-row-table.ts, the one insert all of Orca's writes go through: appends on the single ordered writer, epoch rollover and epoch replacement). The serialized row is parsed with the same reader replay uses, inside the write's transaction; a row it rejects throws journal_row_rejected and rolls the write back. A per-chat file's verbatim copy is checked by folding it as it is copied.
  • Damage fails the load through the same refusal function with journalCorrupt ("Unable to load this chat."): a row that does not parse, a row numbered against its key, a gap, or a missing epoch row. Replay reads on past damage, so a newer Orca's row later in the chat still gives the newer-Orca reason.
  • Removed: deleting the rest of a chat from a damaged row, the row saying part of the history couldn't be loaded, the stored rebuild marker, the rebuild from the agent's transcript (and the adapter's transcript-path hook it alone used), the validation-error classifier that guessed "newer Orca" from a value it did not recognise, and the read-only mode: the journal's read-only latch and every check of it (history pages, catch-up, the status projection, the queue, rewind, teardown), read-only paging from the opened snapshot, the history page's readOnly field and the reconnect without a cursor that read it, the locked composer and its placeholder, and the greyed-out controls. The journal_repairs table stays in the schema, unused here, because older builds on the same schema version still read it.
  • Startup restore (structured-agent-session-restart-restore.ts) catches each chat's failure, logs it for that chat, and carries on.
  • Restart offers (structured-agent-session-restart-resume-host.ts, -resume-set.ts, -failure-ledger.ts): listing already opens each offered chat. The open's refusal now comes back with the reveal (host-side only: agentSession.reveal publishes its named fields and nothing more), and a chat refused as a newer Orca's is skipped (createNewerOrcaChats, re-derived by every reveal, stored nowhere). Its offer stays untouched in the offers file every Orca on the host shares, and nothing is written for it; a failure filed for it is kept but not shown. A newer-Orca refusal met while sending files nothing, and the offer is reopened. "Dismiss all" (agentSession.restartResumableDismiss with no ids, unchanged on the wire, so older clients get it too) reveals every record's chat first and clears the offers file except a newer Orca's chats (clearAll with a keep predicate, still fencing late writers).
  • An approval of an unknown subject kind (agent-session-approval-subject.ts, used by NativeChatApprovalCard.tsx and MobileNativeChatPermission.tsx): the cards check the subject against the kinds this build draws (a test keeps that list equal to the schema's), show the detail and the new line, disable the answer buttons, and keep the existing cancel, which the host routes per provider. The subject's type is open, so code that reads a plan has to narrow first; the host's prompt bounding carries an unknown subject as it was.
  • A chat whose only pending requests cannot be answered here (pendingPromptsAllUnanswerableHere): desktop keeps the composer, and desktop and phone send without delivery: 'queue-if-active' (use-structured-agent-session.ts, use-mobile-structured-send-with-outcome.ts). The host's queue waits on any pending request, so the send would otherwise sit there; sent plainly, it starts a turn. No wire change.
  • Code that rebuilds a stored body must carry unknown kinds. The item body's type names only the kinds this build knows (TypeScript cannot type "any string except these"); readers compare against known kinds, so an unknown item matches none. The code that builds a new body from a stored one (prompt bounding, stale and terminal settlement, the rewind normalizer and merge) carries an unknown kind or subject through unchanged.
  • Rewind carries an item of an unknown kind as it was, in its place, after the row it followed that the provider still holds.

Mixed versions

  • Older clients against this host: a released host served a chat with a newer row version as read-only pages; this host refuses the read instead (an existing read answering differently). The newest release words the refusal with its own sentences, "Chats were saved by a newer Orca. Update Orca to keep using them."; releases before the reason existed (v1.4.218 and older) fall back to "Orca couldn't read this chat's saved history." Both keep retrying, nothing goes wrong, and the host logs the refusal once per chat, with where the load failed (the row's sequence and, for damage, its cause), until the chat opens or closes or the reason changes (createJournalOpenReadRefusals). agent-session-newer-chat-refusal.unit.test.ts folds this host's refusal through the released readers and checks that neither calls it damage.
  • This client against older hosts: they keep such a chat read-only and serve its pages, so this client shows the history and a normal message box; a send is refused with "Chats were saved by a newer Orca. Update Orca to keep using them." and still offers a Retry that cannot succeed. That is what released clients do today. The readOnly page field this PR first added was never released, so removing it reaches no client.
  • A resent message against a history store a newer Orca wrote: the host answers it "outcome unknown" rather than with the recorded answer, because this build never reads that store (the existing resend rule for a chat that will not open). Nothing is sent again and nothing is written; the pane keeps only "This chat was saved by a newer Orca. Update Orca to open it.", and no second message appears on desktop or phone (structured-agent-session-resend-answer.test.ts, MobileNativeChatView.test.ts).
  • Downgrades: builds from before this change still delete history from a newer value inside a known row, and still repair damage by deleting. A cross-version test shows a recent main build deleting a new body kind and a new plan-subject kind while this build keeps them writable. So a new value in a fixed list requires a row-version bump.
  • Older clients against a newer host: a client from before this change shows an approval of a new subject kind with Approve live. So no host may add a subject kind until clients can be told apart (Temporary item below).

Why

  • Fail the load on damage instead of deleting, and refuse damage at write time. The repair was written for a half-written last row, which SQLite transactions do not produce. The source that remains is Orca's own writer and reader disagreeing, and checking each row against the reader as it is written closes it. Deleting from the first bad row also took everything after it, including rows a newer Orca wrote. Failing the load keeps the evidence and the rows, at the cost of a chat you cannot continue.
  • Fail the load for a newer Orca's chat too, instead of a read-only mode. A read-only chat needed a lock on every control, a reason on the composer, its own paging and reconnect, and a carve-out in resume, all for a chat nothing in this build could continue. Failing the load keeps every row for the updated Orca, needs none of that, and its own reason tells the person the one step that helps.
  • Keep unknown content writable, refuse only what cannot be placed. A new kind of message is a row this build can skip safely. A new kind of row, change or sent message changes what the history means, so reading on would misstate it.
  • Each failure said once. A chat that cannot load shows why in its pane and nothing to type into, because a send there could only be refused with the same failure.

Differences from the common pattern

  • Matches: a stored record that cannot be decoded fails the load, and nothing is deleted on read. This now includes a newer build's records.
  • Matches: an unknown message, block or goal kind is kept as it was and the chat stays writable; an empty or whitespace-only kind is damage.
  • Intended: every row is read back with the reader before it is written, and a row it rejects is refused. Orca's reason: its writer and reader are separate code that can drift, and a row the reader rejects would otherwise fail the whole chat's next load.
  • Pre-existing, unchanged: an unreadable optional annotation (a turn's context usage) is dropped on read.
  • Intended: a chat a newer Orca saved fails its load with its own reason and words ("Update Orca to open it") rather than the damage words. Orca's reason: downgrades and second machines meet this routinely, and the only step past it is an update; telling it apart from damage tells the person that.
  • Intended: a chat that fails to load for good shows only why, in its whole pane, whatever was on screen before. Orca's reason: anything left to press there (a message box, Stop, a card, a queued message) can only produce a second refusal of the same failure.
  • Intended: the phone shows these sentences in English, as it shows every other chat notice.
  • Intended: after a restart, a chat a newer Orca saved is not offered to continue here, and its offer is left untouched. Orca's reason: nothing in this build can continue it, and the offers file is shared by every Orca on the host, so the newer one can still act on it.
  • Intended: an approval whose subject this build cannot draw can only be cancelled, with its text shown and one line saying it needs a newer Orca. Orca's constraint behind it: the text Orca stores for an approval can be empty or shortened, so approving would mean approving something not fully shown. On Claude the cancel turns the request down and the turn goes on; on Codex it ends the turn. The chat stays writable either way.
  • Intended: while every pending request is one this build cannot answer, a message is sent at once instead of joining the queue. Orca's constraint behind it: the host's queue waits on any pending request, and nothing in this build can settle that one, so a queued message would wait forever.
  • Intended (mixed versions): an older app connected to this host meets a refused read where it used to get a read-only chat. It shows its own words for it ("Chats were saved by a newer Orca. Update Orca to keep using them." from the newest release, "Orca couldn't read this chat's saved history." from older ones) and keeps retrying; nothing goes wrong, and the host logs it once per chat.
  • Intended: against an older Orca that keeps a newer Orca's chat read-only, a refused send in this client still offers a Retry that cannot succeed, as released clients do today. Orca's constraint behind it: what an older host does is fixed by that build, and this client gets no sign from it that the chat is read-only until a send is refused.
  • Temporary: a damaged chat offers no Retry and no way back except a new chat, and resume still offers a damaged chat and then lists that it could not continue it. Follow-up: a separate decision on how a chat that cannot load is shown and recovered.
  • Intended (known limit): a value a newer build adds to a fixed list of allowed values at the same row version is damage to this build, so the chat fails to load as damaged. A newer build must bump the row version whenever it adds a value to a closed set, so older builds say "Update Orca" instead; journal-closed-sets.test.ts turns a forgotten bump into a CI failure. Orca's constraint behind it: the old guess at "a newer build's value" could not tell a newer value from Orca's own bad write.
  • Temporary: builds from before this change still delete from a newer value inside a known row and still repair damage by deleting. A new value in a fixed list requires a row-version bump either way. Follow-up: define the support window, deferred from fix(native-chat): an older Orca keeps a chat with a newer row kind read-only instead of deleting the rest of its history #24477's review.
  • Temporary: a card's cancel does nothing when no turn is running (a background agent's request), as before this change, because the host's cancel request has to name a turn. For an approval this build cannot draw, that arises only against a newer Orca: a request left by an agent run that has ended is cancelled when the chat opens. The message box stays usable beside the card, and the next turn's cancel works. Follow-up: let a prompt's cancel name no turn (a request-schema change and a new capability), together with STA-9262.
  • Temporary: clients from before this change leave Approve live for an approval subject of a kind they do not know, so a future host that adds a subject kind could have such a user approve it blind. Follow-up: STA-9262, a client capability the host checks before sending a new subject kind; journal-approval-subject-kinds.test.ts fails if a kind is added before that exists.

Linked Issue

Follow-up to #24477 (an item from its review, and the "one malformed optional fact deletes all later history" problem found in #24478's review).

Visual Proof

Desktop, Windows, a dev build of d7281e3 with a stand-in agent (the last change after it is host-side only):

  • A chat a newer Orca saved: only its line, no transcript or message box.
    newer-Orca chat
  • A damaged chat: only "Unable to load this chat.", no message box.
    damaged chat
  • A history store from a newer Orca: every chat shows the newer-Orca line.
    newer store
  • A chat holding a newer Orca's message kind stays writable and sends.
    writable chat with newer content
  • After a restart, the resume dialog lists only the chat this build can continue.
    resume dialog

Phone not captured: no iOS Simulator machine was available for this run.

Testing

Unit and cross-version tests, explicit file lists, vitest --config config/vitest.config.ts (phone: the mobile package's own vitest):

  • A newer Orca's chat: journal-unknown-row-kind.test.ts, journal-unknown-content.test.ts, journal-store-schema.test.ts, journal-damage.test.ts, journal-streaming-replay.test.ts and journal-file-format-remnant.test.ts: a newer row kind, batch-change kind, sent-message kind, row version or history store (including one whose tables changed, and a chat it never held) fails the open twice with journalWrittenByNewerOrca and every row byte-identical; a newer row past damage gives the newer reason; once the row's kind is known, the chat opens whole and takes writes. journal-per-session-import.test.ts (+1): a newer per-chat file is refused by the preview, the copy and the open, stays byte-identical, and nothing of it is published. structured-agent-session-unopened-tab.test.ts (2), on a real host: a restored chat that is damaged, or a newer Orca's, keeps its tab, and its history read and its send are refused with its own reason. structured-agent-session-queued-newer-orca.test.ts (2): the history read, a queued send, Send-now, Delete and Resume are all refused with the update words. structured-agent-session-startup-tab-restore.test.ts, structured-agent-session-startup-reconcile-failure.test.ts and structured-chat-tab-table.test.ts: over a newer Orca's records every chat tab is listed, nothing is written, and no chat opens.
  • Restart offers: structured-agent-session-restart-offer-newer-orca.test.ts (7), on a real host: a newer Orca's chat (a newer row, or a newer history store) is not offered, nothing runs and nothing is filed, and the offers file is byte-identical after the listing and after an attempt to continue it; a newer-Orca refusal at send time files nothing and keeps the offer; a failure filed before the chat became a newer Orca's is kept but not shown; a damaged chat is still offered and its failure is filed with journalCorrupt; "Dismiss all", with or without a listing first, removes the listed chat's offer and leaves a newer Orca's hidden offer byte-identical. Ten runs in a row pass. structured-agent-session-restart-resume.test.ts (2): the set neither offers nor spends a newer Orca's chat, and a fork still ends its offer.
  • Logged once: journal-open-failure.test.ts (+2): a load refused as a newer Orca's chat or as damage is logged once per chat until it opens or closes, and again when its reason changes, each line naming the row's sequence and the damage's cause. structured-agent-session-unopened-tab.test.ts: three refused reads of such a chat on a real host leave one log line.
  • Said once, whole pane: NativeChatStructuredSession.final-read-failure.test.tsx (3): with the transcript, an approval, a goal, a queued card and a background task on screen, a damaged read and a newer Orca's read each leave only their words; a read that can clear keeps all of it. MobileNativeChatView.test.ts: a final failure over a loaded transcript leaves only its words (no list, composer or Stop); one that can clear keeps the transcript and composer.
  • Words: agent-session-refusal-notice.test.ts (+1): the read-history words are the two new sentences, and writes keep the plural ones. structured-agent-session-read-transport.test.ts (2): damage and a newer Orca's chat stop the reconnects; an open that can clear keeps them. NativeChatStructuredSession.read-error.test.tsx: a damaged chat and a newer Orca's chat each say their line once, with no "trying again" and no composer; a failure that can clear keeps the composer. Phone: use-mobile-structured-agent-session-read-failure.test.tsx (3): the read failure carries its refusal, damage and a newer chat are final in their words, and an open that can clear is not; mobile-structured-agent-session-thrown-refusal.test.ts (+1).
  • Write-time check: journal-damage.test.ts: a row the reader would reject is refused when written, the next write lands and the chat reopens with every other row; an epoch replacement holding one is rolled back whole. structured-agent-session-surface-lifetime.test.ts (+1) and structured-agent-session-newer-content-host.test.ts, on a real host: a provider's output the reader would reject mid-turn stops that provider, the turn ends as interrupted with "Orca ran into a problem, so this didn't go through. Try again.", nothing restarts it on its own, the next send starts a child, and the reopened chat still holds the rows written before.
  • Damage: journal-damage.test.ts: a row that is not a row, a known row missing a field, a row numbered against its key, a gap and a missing epoch row each fail the load twice in a row with every stored row unchanged; the damage is named by sequence and cause. journal-per-session-import.test.ts: a damaged per-chat file is refused by the startup preview, by the copy and by the open, and stays byte-identical. An epoch, or a per-chat file, with no rows opens as a fresh chat.
  • Newer content inside a known row: journal-unknown-content.test.ts: a new body kind, a new plan-subject kind and a batch change carrying a new body kind stay writable with every earlier row byte-identical; context usage of a newer shape is dropped. agent-session-journal-body-admission.test.ts, journal-closed-sets.test.ts, structured-agent-session-unknown-item.test.ts: an unknown item makes no turn or prompt. NativeChatApprovalCard.test.tsx and MobileNativeChatPermission.test.ts: an unknown subject says it needs a newer Orca, answers nothing, and only its cancel reaches the host; a plan stays answerable. structured-agent-session-prompt-cancel.test.ts (+2). NativeChatStructuredSession.newer-approval.test.tsx (4), use-structured-agent-session.queued-gating.test.tsx (+1), use-mobile-structured-agent-session-newer-approval-send.test.tsx (2), agent-session-approval-subject.test.ts and structured-agent-session-newer-content-host.test.ts: the composer stays beside a card this build cannot answer, sends go out unqueued, and on a real host a plain send reaches the agent and the next turn's cancel settles the card. Rewind: structured-rewind-journal-body.test.ts, structured-rewind-retained-host-rows.test.ts and structured-agent-session-rewind.test.ts.
  • Startup restore: structured-agent-session-restart-restore.test.ts (+1): one chat whose open throws is logged and the other eleven open.
  • Cross-version: agent-session-newer-chat-refusal.unit.test.ts (4): v1.4.218 words this host's newer-chat refusal as an unreadable history (its code's words), never as damage, and keeps retrying; the newest release words it as its own, never as damage; this build keeps a newer body kind and plan-subject kind writable where a recent main build deletes them. agent-session-stop-event-downgrade.unit.test.ts (3): this build refuses a newer row kind and keeps it, where an older build deletes it.

Regression runs (after merging main a154562, which adds the resend answer from #25158, the "Not sent" and "Sending…" delivery notices and the cut-turn notice): the journal, session-wire, runtime, agent-hooks, desktop native-chat and shared session suites, every test file this PR touches, and every test file main changed in those areas, 1,778 files / 20,227 tests, pass (1 file and 45 tests skipped). Every cross-version unit test, one at a time: 18 files / 109 tests pass. Phone session suites: 227 files / 2,201 tests pass.

Ablations (each piece reverted alone; every one goes red): a newer chat's open not refused (8), damage winning over a newer row (1), a newer chat's read not final (desktop 2, phone 1), a newer chat's restart offer spent by the listing (3), a newer-Orca refusal at send time spending the offer (1), a newer Orca's filed failure shown (1), a newer or damaged load refusal not logged (3), "Dismiss all" clearing a newer Orca's hidden offer (2), "Dismiss all" not revealing the chats first (1), the refusal dropping where it failed (1), the log keyed without the reason (1), the read-history words falling back to the plural ones (desktop 2, phone 1), a composer under a final failure on desktop / phone (2 / 1), a final failure over a loaded transcript leaving it and its controls on desktop (2 + 2) / phone (1), a newer per-chat file skipped instead of refused (1), a newer history store opening its chats (2), the phone dropping the read's refusal (2). From earlier rounds: unknown body kind not kept (5), unknown plan subject not kept (3), empty tag accepted (1), one failing chat stops startup restore (2), damage does not fail the load (10), damaged per-chat file copied (1); unknown-subject Approve live on desktop / phone (2 / 2), its line missing (2 / 1), every subject treated as drawable (2 + 2); the composer hidden beside a card this build cannot answer (1), desktop / phone still queueing beside one (1 / 1), a question or known approval counted as unanswerable (3 + 1).

Checks: tc:node, tc:web, tc:cli, mobile tsc and the mobile tests-typecheck ratchet pass; oxlint, anti-slop, the changed-code quality gate and React Doctor report nothing; the localization verifiers (catalogs, catalog, runtime catalog, extraction, coverage) and the reliability-gate manifest check pass.

Not verified here: live desktop and phone QA of this round, and Linux.

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

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.

Notes

  • SSH and remote hosts: the host that owns the chat decides whether it loads, and its refusal travels on the read every client already makes, so a desktop or phone attached to a remote Orca behaves as a local one.
  • Folder workspaces: unaffected; chat history is per session whatever the workspace kind.
  • Performance: a row that parses costs one parse on read, as before. Every insert now also parses the row it writes, inside the write's transaction: about 9 µs for a 3 KB row and about 145 µs for a 256 KB one. A per-chat file is checked while it is copied, from rows already read.
  • Cross-platform: no platform-specific code.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…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.
…e 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.
…epairs 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.
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.
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.
@brennanb2025 brennanb2025 changed the title fix(native-chat): newer message content opens a chat read-only with a notice, never deleting history fix(native-chat): an older Orca keeps a chat with a newer Orca's content read-only, never deleting history, and the composer says why Oct 2, 2026
…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).
… 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.
…ly 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.
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.
- 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.
…n 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.
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.
…ed 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.
…eason

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.
…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.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status (head 1150b05, still a draft)

The problem. #24477 stopped an older Orca from deleting a chat's history when it meets a row kind a newer Orca wrote. Three gaps were left:

  • A new kind of content inside a known row (a new message card, a new kind of batch change, a new kind of sent-message content) was still treated as damage. Orca deleted that row and every row after it.
  • A chat that opened read-only looked normal. The first sign was a failed send.
  • That read-only chat was also offered under "Resume interrupted chats?". It then said "Orca couldn't resume this chat. Open it to continue manually." and left "N chats failed to resume" in the status bar.

What changes for you, running an older Orca on a chat a newer Orca wrote:

  • Nothing is deleted. Content of a kind this Orca doesn't know keeps every row and opens the chat read-only, the same way a newer row kind already does. It reads whole again after you update.
  • The composer is locked and says why: "Saved by a newer Orca. Update Orca to continue this chat." That is the only place the reason appears. No new banner or toast is added.
    • A pending question or approval stays visible but greyed out.
    • There is no "working" row and no Stop.
    • Queued-message and goal actions, the model and option pickers, and dictation are disabled.
  • History scrolls back. Before, a read-only chat could not load past its first page.
  • No resume offer for a read-only chat. It isn't offered and isn't counted as failed. The offer comes back after you update, unless you used "Dismiss all".
  • Real damage is unchanged. A broken row (invalid JSON, a wrong type, a missing field) is repaired as before.
  • Normal chats behave exactly as on main.

What the review changed (the PR was largely reworked):

  • Removed the per-field policy. It declared every optional field as "droppable" or "must-understand". It was guarding against a failure nobody had observed. It also made damage inside a must-understand field freeze a chat for good while telling you to update, which could never help.
    • Now a chat goes read-only only on real evidence of a newer Orca: a kind or value outside a fixed list this build knows, at any depth, or a newer row version or database.
    • Everything else is damage and gets the existing repair.
  • Fixed: a new kind of batch change with no item id still deleted history.
  • Replaced the separate notice line (desktop status strip and a new phone notice) with the composer's existing locked state. Before, the reason could appear twice after a refused send, and every control stayed clickable and was refused.
  • Fixed: a locked chat stayed locked after the host was updated (reconnect now fetches a whole page).
  • Fixed: a pending approval vanished, and the chat showed "working" forever.
  • Fixed, found in live QA: the editor drew no placeholder while disabled, so the reason never appeared on screen.
  • Folded in the resume-list fix, including a newer Orca's database that can't be opened at all, so a resume offer is never lost.
  • The PR description's claims about the common pattern were corrected.

Deferred (follow-ups):

  • A damaged row is still repaired by deleting it and every later row. This is pre-existing. The follow-up is to never delete on read and to check each row when it is written.
  • Builds from before this change still delete on unknown content. This needs a defined support window (R3-3 from fix(native-chat): an older Orca keeps a chat with a newer row kind read-only instead of deleting the rest of its history #24477).
  • Pre-existing paths where a newer Orca's change is still deleted: a newer Orca raising the batch-size limits, or a damaged earlier row taking newer rows with it.
  • A reconnect drops scrolled-back items in a read-only chat; scrolling back loads them again.
  • A draft already typed into the box hides the reason.
  • Nits recorded in the review ledger.

Verified:

  • Unit and cross-version tests. Each new behaviour was reverted on its own and its tests went red. The newest release's client handles the new optional readOnly page field and the read-only paging.
  • Typechecks pass: tc:node, tc:web, mobile tsc and the mobile test-typecheck ratchet.
  • Lint and gates are clean: oxlint, the changed-code gate, React Doctor and the localization checks.
  • Readiness checklist passed with no P0 or P1.
  • Live desktop QA on Windows (an isolated dev build, a stand-in agent, a chat holding an unknown content kind): history kept, nothing repaired away, the composer locked with the reason drawn, the approval card visible and disabled, no working row or Stop, and a normal chat unaffected. Screenshots are in the description.

Not verified:

  • Phone, live: no iOS Simulator machine was available. The phone was covered by unit tests only.
  • Never exercised live: queued-message and goal-banner controls (the stand-in agent can't queue or set goals), the whole-database-newer trigger, the resume-list fix, and SSH (checked by reading code and the tests only).
  • The visual QA is from 7c17fc7. Everything after it is host-side resume changes and comments.
  • CI at 1150b05: repository guards, verify and test-vs-non-test line count pass. Typecheck, static analysis, cross-version wire and the mobile bundle were still queued on runners when this was posted. An earlier full run failed only on a test helper this PR doesn't touch, and that helper has since been fixed on main and merged in.

…t 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.
…unknown-content-read-only

# Conflicts:
#	src/main/native-chat/agent-session-journal/journal-corruption-repair.test.ts
#	src/main/native-chat/agent-session-journal/journal-repair-disclosure.ts
#	src/main/native-chat/agent-session-journal/journal-store-open.ts
#	src/main/native-chat/agent-session-journal/journal-store-schema.test.ts
#	src/renderer/src/components/native-chat/NativeChatStructuredSession.read-error.test.tsx
#	src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
#	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-refusal-notice.ts
#	src/shared/agent-session-write-notice-copy.ts
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review status (head 5504adc, still a draft, not merged)

The problem. Chat history was being deleted on read. Running an older Orca on a chat a newer Orca had written, any row the older build couldn't parse made it delete that row and everything after it. It then tried to rebuild from the agent's own transcript, so sends, Stops and Resumes that only Orca recorded were lost for good. A damaged row did the same. #24477 stopped this for new row kinds only. New content inside a known row, and damaged rows, still deleted history.

What changes for you.

  • Nothing is deleted when a chat is read. This covers a newer Orca's content, damaged rows, and chat history files from older builds.
  • A chat saved by a newer Orca no longer opens. It says "This chat was saved by a newer Orca. Update Orca to open it." and the whole pane shows only that. Update, and it opens with every row intact. This applies to a new kind of row, change or sent message, a newer row format, and a newer history store (every chat in it).
  • A damaged chat says "Unable to load this chat." and nothing else. You start a new chat to carry on.
  • The failure is said once. No message box sits under a chat that can't load, so a send can't fail a second time with the same reason. This is the same on desktop and the phone, even when the failure arrives after the chat was already on screen.
  • A newer Orca's message or card inside a chat no longer blocks the chat. The chat stays usable and the unknown item is not drawn. An approval request of a kind this Orca doesn't know shows its text with "This request needs a newer version of Orca."; Approve is off and cancel works. If that card is the only thing pending, the message box stays open and a send goes straight through instead of queuing behind it.
  • Orca checks each row before saving it. A row it would later be unable to read is refused at write time, so it can't make the chat unloadable. If an agent's output is refused mid-turn, the turn ends as failed with the usual "try again" message, the next send works, and earlier history stays.
  • Startup. One chat that can't open no longer stops every other chat's tab from coming back. Its tab stays and says why.
  • Resume after restart. A newer Orca's chat is not offered and its offer is left for the newer Orca, including through "Dismiss all". A damaged chat is still offered (Temporary, below).
  • The read-only mode from fix(native-chat): an older Orca keeps a chat with a newer row kind read-only instead of deleting the rest of its history #24477 is gone. A newer Orca's chat used to open read-only, with a locked message box. It now fails to load, as described above.

How it got here. The PR was largely reworked during review, after the common pattern was checked against the design.

  • An early per-field "droppable / must-understand" policy was removed. It froze damaged chats behind an "Update Orca" that couldn't help.
  • The proactive status-strip notice became the composer's existing locked state, and was then removed with the read-only mode once Brennan chose "fail the load".
  • Fixes found in review or QA:
    • startup restore aborting every tab;
    • a damaged chat still showing a message box;
    • an unknown approval crashing the attach step;
    • rewind dropping unknown items;
    • a chat stuck behind an unanswerable card;
    • an older build spending a newer build's resume offers;
    • several stale comments and PR-text claims.
  • Main was merged twice. fix(native-chat): one ordered journal writer #24127's single journal writer keeps the save-time check, and main's repair-wording change was copy only.

Deferred / follow-ups.

  • A damaged chat has no Retry or recovery beyond a new chat, and resume still offers it and files "couldn't resume" (Temporary).
  • Builds from before this change still delete history on unknown content or damage. Needs a defined support window.
  • A newer build must bump the row version whenever it adds a value to a fixed list. journal-closed-sets.test.ts turns a forgotten bump into a CI failure.
  • A new approval kind needs a client-version check first; older clients would let it be approved blind. Tracked in STA-9262, and journal-approval-subject-kinds.test.ts blocks adding one before then.
  • A card's cancel with no running turn does nothing. The host request schema needs a turn; follow-up with STA-9262.
  • Older released apps against this host show their own wording for a newer-Orca chat and keep retrying. Nothing wrong happens, and the host logs it once per chat.
  • The item body type names only known kinds; code that rebuilds a stored body must carry unknown kinds, and tests pin this. A fully open type would mean about 170 files of narrowing.

Verified.

  • Tests: unit, real-host and cross-version tests, each new behaviour ablated (reverted alone, tests went red). The last full runs: 913 files / 9,445 tests, cross-version 16 files / 98 tests, phone 210 files / 2,012 tests.
  • Typechecks: tc:node, tc:web, tc:cli, mobile tsc and the tests ratchet.
  • Lint and gates: oxlint, anti-slop, the changed-code gate, React Doctor, localization and reliability gates.
  • Review: five review rounds plus narrow checks and two readiness checklists, with no P0/P1 open.
  • CI at 5504adc: 18 pass, 0 fail.
  • Live desktop QA on Windows (isolated dev build, stand-in agent) at d7281e3: a newer-Orca chat, a newer database, a damaged chat, a writable chat holding newer content, a normal chat, and the resume dialog all behaved as described. Screenshots are in the description.

Not verified.

  • The phone, live: no iOS Simulator machine was available.
  • A live approval request of an unknown kind, and the stuck-chat case. The test machine's host cancels leftover requests on open, so real-host tests cover these.
  • Newer batch-change / sent-message kinds, a newer row version and a newer per-chat file in the live app. Unit tests only.
  • SSH, by code and tests only.
  • The Dismiss-all fix (5504adc) after the last QA, by real-host test only.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 3, 2026 14:36

@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: 628dd6fb-777b-4c75-a8a9-c2834db1f1a5
📥 Commits

Reviewing files that changed from the base of the PR and between 3cca472 and 5504adc.

📒 Files selected for processing (166)
  • config/reliability-gates.jsonc
  • mobile/src/session/MobileNativeChatOverlay.tsx
  • mobile/src/session/MobileNativeChatPermission.test.ts
  • mobile/src/session/MobileNativeChatPermission.tsx
  • mobile/src/session/MobileNativeChatView.test.ts
  • mobile/src/session/MobileNativeChatView.tsx
  • mobile/src/session/mobile-structured-agent-session-rpc.ts
  • mobile/src/session/mobile-structured-agent-session-thrown-refusal.test.ts
  • mobile/src/session/use-mobile-native-chat-session.ts
  • mobile/src/session/use-mobile-structured-agent-session-newer-approval-send.test.tsx
  • mobile/src/session/use-mobile-structured-agent-session-read-failure.test.tsx
  • mobile/src/session/use-mobile-structured-agent-session.ts
  • mobile/src/session/use-mobile-structured-agent-state.ts
  • mobile/src/session/use-mobile-structured-send-with-outcome.ts
  • src/main/agent-hooks/first-work-branch-rename.test.ts
  • src/main/claude/claude-background-wake-retention.test.ts
  • src/main/claude/claude-structured-prompt-items.ts
  • src/main/codex/codex-close-turn-end.test.ts
  • src/main/codex/codex-requested-close-turn-timing.test.ts
  • src/main/codex/codex-structured-session-acquire.ts
  • src/main/codex/codex-structured-session-adapter-lifecycle.test.ts
  • src/main/codex/codex-structured-session-adapter.test.ts
  • src/main/codex/codex-structured-session-adapter.ts
  • src/main/codex/codex-structured-session-close.test.ts
  • src/main/codex/codex-structured-session-options-catalog.test.ts
  • src/main/codex/codex-structured-session-options.test.ts
  • src/main/codex/codex-structured-session-state.ts
  • src/main/codex/codex-structured-thread-facts.ts
  • src/main/codex/codex-structured-thread-open.test.ts
  • src/main/codex/codex-structured-thread-open.ts
  • src/main/native-chat/agent-session-journal/journal-approval-subject-kinds.test.ts
  • src/main/native-chat/agent-session-journal/journal-closed-sets.test.ts
  • src/main/native-chat/agent-session-journal/journal-corruption-repair.test.ts
  • src/main/native-chat/agent-session-journal/journal-cursor.ts
  • src/main/native-chat/agent-session-journal/journal-damage.test.ts
  • src/main/native-chat/agent-session-journal/journal-database-schema.ts
  • src/main/native-chat/agent-session-journal/journal-database.test.ts
  • src/main/native-chat/agent-session-journal/journal-epoch-controller.ts
  • src/main/native-chat/agent-session-journal/journal-epoch-replacement.ts
  • src/main/native-chat/agent-session-journal/journal-epoch-rollover.ts
  • src/main/native-chat/agent-session-journal/journal-file-format-remnant.test.ts
  • src/main/native-chat/agent-session-journal/journal-host-database-test-support.ts
  • src/main/native-chat/agent-session-journal/journal-legacy-import.test.ts
  • src/main/native-chat/agent-session-journal/journal-legacy-import.ts
  • src/main/native-chat/agent-session-journal/journal-open-failure.test.ts
  • src/main/native-chat/agent-session-journal/journal-open-failure.ts
  • src/main/native-chat/agent-session-journal/journal-open.ts
  • src/main/native-chat/agent-session-journal/journal-per-session-import.test.ts
  • src/main/native-chat/agent-session-journal/journal-per-session-import.ts
  • src/main/native-chat/agent-session-journal/journal-per-session-source.ts
  • src/main/native-chat/agent-session-journal/journal-prompt-body-bounds.ts
  • src/main/native-chat/agent-session-journal/journal-repair-disclosure.ts
  • src/main/native-chat/agent-session-journal/journal-repair-marker.ts
  • src/main/native-chat/agent-session-journal/journal-row-context-usage.test.ts
  • src/main/native-chat/agent-session-journal/journal-row-schema.test.ts
  • src/main/native-chat/agent-session-journal/journal-row-schema.ts
  • src/main/native-chat/agent-session-journal/journal-row-table.ts
  • src/main/native-chat/agent-session-journal/journal-stop-and-resume-rows.test.ts
  • src/main/native-chat/agent-session-journal/journal-store-collaborators.ts
  • src/main/native-chat/agent-session-journal/journal-store-open.ts
  • src/main/native-chat/agent-session-journal/journal-store-restore.ts
  • src/main/native-chat/agent-session-journal/journal-store-schema.test.ts
  • src/main/native-chat/agent-session-journal/journal-store.test.ts
  • src/main/native-chat/agent-session-journal/journal-store.ts
  • src/main/native-chat/agent-session-journal/journal-streaming-replay.test.ts
  • src/main/native-chat/agent-session-journal/journal-unknown-content.test.ts
  • src/main/native-chat/agent-session-journal/journal-unknown-row-kind.test.ts
  • src/main/native-chat/agent-session-journal/journal-write-guards.ts
  • src/main/native-chat/agent-session-journal/queued-message-bookkeeping-failure.test.ts
  • src/main/native-chat/agent-session-journal/queued-message-store.test.ts
  • src/main/native-chat/agent-session-wire/agent-session-history-page.ts
  • src/main/native-chat/agent-session-wire/agent-session-journal-recovery.test.ts
  • src/main/native-chat/agent-session-wire/agent-session-journal-recovery.ts
  • src/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-acquisition-options.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-attach-context.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-attach-test-conversation.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-command-publication.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-conversation-open.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-forget-status.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-delivery.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-tabs.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-types.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-newer-content-host.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-newer-orca.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-wiring.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-read-restore.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-readable-restorer.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-refusal-retry.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-candidates.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-failure-ledger.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-newer-orca.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-records.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-restore.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-restore.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-set.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-test-harness.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-wiring.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-reveal.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-rewind.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-rewind.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-settled-attach-retry.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-startup-reconcile-failure.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-status-feed.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-status-journal-projection.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-surface-lifetime.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-turn-completion-feed.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-unopened-tab.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.ts
  • src/main/native-chat/agent-session-wire/structured-rewind-journal-body.test.ts
  • src/main/native-chat/agent-session-wire/structured-rewind-journal-body.ts
  • src/main/native-chat/agent-session-wire/structured-rewind-retained-host-rows.test.ts
  • src/main/native-chat/agent-session-wire/structured-rewind-retained-host-rows.ts
  • src/main/runtime/agent-session-recovery-capsule-entries.ts
  • src/main/runtime/agent-session-recovery-capsule.ts
  • src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts
  • src/main/runtime/rpc/methods/structured-agent-session-reveal.ts
  • src/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.ts
  • src/main/runtime/rpc/methods/structured-chat-tab-table.test.ts
  • src/main/runtime/structured-agent-session-failure-logging.test.ts
  • src/main/runtime/structured-agent-session-integration-replay.test.ts
  • src/main/runtime/structured-agent-session-startup-tab-restore.test.ts
  • src/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsx
  • src/renderer/src/components/native-chat/NativeChatApprovalCard.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.final-read-failure.test.tsx
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.newer-approval.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-write-notice-text.ts
  • src/renderer/src/components/native-chat/native-chat-view-state.ts
  • src/renderer/src/components/native-chat/structured-agent-session-read-transport.test.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session-delivery-notices.ts
  • src/renderer/src/components/native-chat/use-structured-agent-session.queued-gating.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-approval-subject.test.ts
  • src/shared/agent-session-approval-subject.ts
  • src/shared/agent-session-journal-body-admission.test.ts
  • src/shared/agent-session-journal-body-admission.ts
  • src/shared/agent-session-journal-open-union.ts
  • src/shared/agent-session-journal-schemas.ts
  • src/shared/agent-session-journal-types.ts
  • src/shared/agent-session-refusal-notice.test.ts
  • src/shared/agent-session-refusal-notice.ts
  • src/shared/agent-session-refusal-reason-words.ts
  • src/shared/agent-session-write-notice-copy.ts
  • src/shared/structured-agent-session-read-refusal.ts
  • src/shared/structured-agent-session-unknown-item.test.ts
  • tests/e2e/cross-version-wire/agent-session-newer-chat-refusal.unit.test.ts
  • tests/e2e/cross-version-wire/agent-session-stop-event-downgrade.unit.test.ts
💤 Files with no reviewable changes (25)
  • src/main/codex/codex-structured-thread-open.test.ts
  • src/main/codex/codex-structured-session-acquire.ts
  • src/main/native-chat/agent-session-journal/queued-message-bookkeeping-failure.test.ts
  • src/main/codex/codex-structured-session-close.test.ts
  • src/main/native-chat/agent-session-wire/agent-session-history-page.ts
  • src/main/native-chat/agent-session-wire/agent-session-journal-recovery.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-rewind.ts
  • src/main/codex/codex-structured-session-state.ts
  • src/main/codex/codex-structured-thread-facts.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-adapter.ts
  • src/main/native-chat/agent-session-journal/journal-corruption-repair.test.ts
  • src/main/codex/codex-structured-session-options-catalog.test.ts
  • src/main/native-chat/agent-session-journal/journal-row-context-usage.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-read-restore.test.ts
  • src/main/codex/codex-close-turn-end.test.ts
  • src/main/codex/codex-structured-session-adapter.test.ts
  • src/main/native-chat/agent-session-journal/journal-repair-marker.ts
  • src/main/codex/codex-requested-close-turn-timing.test.ts
  • src/main/native-chat/agent-session-journal/journal-repair-disclosure.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-readable-restorer.test.ts
  • src/main/native-chat/agent-session-journal/journal-store-collaborators.ts
  • src/main/codex/codex-structured-session-options.test.ts
  • src/main/native-chat/agent-session-wire/agent-session-journal-recovery.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-attach-context.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/ja.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/ko.json
  • src/main/native-chat/agent-session-journal/journal-row-schema.test.ts
  • 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; 9 remain after this review.

"invariant": "Replay preserves latest revisions, original item order, fences, aliases, submissions, a newer schema winning over damage and cursor cleanup while retaining live items rather than all historical bodies; damage fails the load and deletes nothing.",
"oracle": "Replay 2,048 16 KiB revisions into one latest item with less than 8 MiB sampled live heap growth; name a future-schema row read past a gap, name the first damage, fail a damaged load with every row kept, and hold no SQLite read snapshot across reduction (a mid-replay checkpoint is not busy). Existing journal and subscriber tests cover replayed content.",
"commands": [
// As the 2026-09-11 evidence run ran it; the recovery test it names went with the read-time repair.

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
fd -t f 'agent-session-journal-recovery' src
fd -t f 'journal-damage.test.ts|journal-streaming-replay.test.ts' src

Repository: stablyai/orca

Length of output: 293


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- current gate ---'
sed -n '630,675p' config/reliability-gates.jsonc
printf '%s\n' '--- merge-base diff ---'
git diff --unified=8 62451920ed161a342dc870f93a570d4cd126428b 5504adc2452aa69c76949047e283804827d7719e -- config/reliability-gates.jsonc | sed -n '1,220p'
printf '%s\n' '--- vitest command and relevant notes ---'
rg -n -C 3 'vitest|agent-session-journal-recovery\.test\.ts|journal-damage\.test\.ts|missing named|filter' package.json config src/main/native-chat/agent-session-journal --glob '!**/*.snap' | head -240
printf '%s\n' '--- recovery test files tracked at head ---'
git ls-tree -r --name-only 5504adc2452aa69c76949047e283804827d7719e -- src | rg 'agent-session-journal-recovery|journal-(damage|streaming-replay)\.test\.ts' || true
printf '%s\n' '--- base gate ---'
git show 62451920ed161a342dc870f93a570d4cd126428b:config/reliability-gates.jsonc | sed -n '630,675p'

Repository: stablyai/orca

Length of output: 38244


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- gate command consumers ---'
rg -n -C 4 'commands|evidenceRuns|reliability-gates' config/scripts/check-reliability-gates.mjs config --glob '*.md' --glob '*.mjs' --glob '*.jsonc' | head -240
printf '%s\n' '--- gate checker ---'
sed -n '1,240p' config/scripts/check-reliability-gates.mjs
printf '%s\n' '--- project notes about missing test filters ---'
rg -n -i -C 3 'missing (named )?(test|file)|test path|file filter|vitest.*filter|filter.*vitest|silently.*(skip|run)|no test files' config/reliability-gates.jsonc config --glob '*.md' --glob '*.jsonc' --glob '*.mjs' | head -200

Repository: stablyai/orca

Length of output: 41686


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- exact stale-path guidance in reliability gates ---'
rg -n -i -C 4 'silently (skip|run)|missing (named )?(file|path)|stale (command|path)|deleted.*(command|path)|command.*(deleted|stale)' config/reliability-gates.jsonc
printf '%s\n' '--- checker command and evidence rules ---'
sed -n '110,210p' config/scripts/check-reliability-gates.mjs

Repository: stablyai/orca

Length of output: 41333


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- gate commands and evidence runs ---'
nl -ba config/reliability-gates.jsonc | sed -n '650,730p'
printf '%s\n' '--- repository precedent ---'
nl -ba config/reliability-gates.jsonc | sed -n '5261,5264p'
printf '%s\n' '--- checker requirements ---'
nl -ba config/scripts/check-reliability-gates.mjs | sed -n '122,130p;196,202p'

Repository: stablyai/orca

Length of output: 17232


🏁 Script executed:

#!/bin/bash
printf '%s\n' '--- recovery-test path at merge base ---'
git ls-tree -r --name-only 62451920ed161a342dc870f93a570d4cd126428b -- src | rg 'agent-session-journal-recovery\.test\.ts' || true
printf '%s\n' '--- recovery-test path at reviewed head ---'
git ls-tree -r --name-only 5504adc2452aa69c76949047e283804827d7719e -- src | rg 'agent-session-journal-recovery\.test\.ts' || true
printf '%s\n' '--- PR status for the path and journal tests ---'
git diff --name-status 62451920ed161a342dc870f93a570d4cd126428b 5504adc2452aa69c76949047e283804827d7719e -- src/main/native-chat/agent-session-wire src/main/native-chat/agent-session-journal | rg 'recovery|corruption|damage|streaming' || true

Repository: stablyai/orca

Length of output: 746


Retire the command that names the removed recovery test.

agent-session-journal-recovery.test.ts is deleted in this PR. The 2026-09-11 command can report success without running that path. Remove that command and its matching evidence run; keep the 2026-10-02 run as current evidence.

Suggested cleanup
       "commands": [
-        // As the 2026-09-11 evidence run ran it; the recovery test it names went with the read-time repair.
-        "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts src/main/native-chat/agent-session-wire/agent-session-journal-recovery.test.ts",
         "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts",
...
       "evidenceRuns": [
-        {
-          "date": "2026-09-11",
-          "runner": "local",
-          "platform": "macos",
-          "command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/native-chat/agent-session-journal src/main/native-chat/agent-session-wire/agent-session-history-page.test.ts src/main/native-chat/agent-session-wire/agent-session-history-forward-read-budget.test.ts src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.ts src/main/native-chat/agent-session-wire/agent-session-journal-recovery.test.ts",
-          "result": "passed",
-          "durationSeconds": 7.71,
-          "summary": "245 tests passed across 22 files. Retained-heap oracle fails on baseline at 68.6 MB and passes under 8 MiB with streaming; gap/schema and cursor-cleanup assertions passed."
-        },
         {
           "date": "2026-10-02",

Comment on lines +28 to +31
const unknown = z
.object({ [key]: z.string().regex(/\S/) })
.refine((value) => !tags.has(value[key] ?? ''), { abort: true })
return z.union([known, unknown])

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
# Where rewind records and journal body/render-item schemas are parsed, and whether .data/.parse output is used.
rg -nP -C3 'AgentSessionRewindRecordSchema\.(parse|safeParse)' --type=ts
rg -nP -C3 '(AgentJournalItemBodySchema|AgentJournalRenderItemSchema)\.(parse|safeParse)' --type=ts
rg -nP -C2 'AgentJournalRenderItemSchema|AgentJournalItemBodySchema' --type=ts -g '!**/*.test.ts' | head -80

Repository: stablyai/orca

Length of output: 9466


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- changed-file diff against PR base ---'
git diff 62451920ed161a342dc870f93a570d4cd126428b 5504adc2452aa69c76949047e283804827d7719e -- src/shared/agent-session-journal-open-union.ts
printf '%s\n' '--- open union source ---'
cat -n src/shared/agent-session-journal-open-union.ts
printf '%s\n' '--- schema and rewind definitions ---'
sed -n '250,370p' src/shared/agent-session-journal-schemas.ts | cat -n
sed -n '1,100p' src/shared/agent-session-rewind.ts | cat -n
printf '%s\n' '--- rewind body helper and call sites ---'
rg -n -C3 'restoreRewindJournalBody|AgentSessionRewindRecord|\.retained\b' src --glob '*.ts' --glob '!*.test.ts'
printf '%s\n' '--- zod dependency/version declarations ---'
rg -n '"zod"|zod@|zod:' package.json pnpm-lock.yaml yarn.lock package-lock.json 2>/dev/null | head -30

Repository: stablyai/orca

Length of output: 41073


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- schema references (source only) ---'
rg -n -C2 'AgentSessionRewindRecordSchema|AgentJournalItemBodySchema|AgentJournalRenderItemSchema|isAgentSessionRewindRecord' src --glob '*.ts' --glob '!*.test.ts'
printf '%s\n' '--- rewind journal body helper ---'
cat -n src/main/native-chat/agent-session-wire/structured-rewind-journal-body.ts
printf '%s\n' '--- rewind retained-row consumer ---'
sed -n '1,150p' src/main/native-chat/agent-session-wire/structured-rewind-retained-host-rows.ts | cat -n
printf '%s\n' '--- session record validation and rewind persistence references ---'
sed -n '320,370p' src/shared/agent-session-record.ts | cat -n
rg -n -C3 'record\.rewind|\.rewind\b|rewind:' src/main src/shared --glob '*.ts' --glob '!*.test.ts' | head -160

Repository: stablyai/orca

Length of output: 36784


🌐 Web query:

site:zod.dev zod 4 object unknown keys stripped by default looseObject

💡 Result:

In **Zod 4**, `z.object()` strips unrecognized keys from the parsed result by default. Use `z.looseObject()` to preserve unknown keys, or `z.strictObject()` to reject them. ([zod.dev](https://zod.dev/api?id=urls&utm_source=openai))

```ts
z.object({ name: z.string() }).parse({ name: "Yeller", extra: true });
// => { name: "Yeller" }

z.looseObject({ name: z.string() }).parse({ name: "Yeller", extra: true });
// => { name: "Yeller", extra: true }
```

Citations:

- 1: https://zod.dev/api?id=urls&utm_source=openai

Preserve unknown fields in the fallback arm.

When openDiscriminatedUnion parses an unknown tag, its fallback object strips every field except the tag from .parse() and .safeParse().data. This breaks the “kept as-is” contract for callers that use parsed output. The existing admission guards only check .success.

🐛 Suggested fix
   const unknown = z
-    .object({ [key]: z.string().regex(/\S/) })
+    .looseObject({ [key]: z.string().regex(/\S/) })
     .refine((value) => !tags.has(value[key] ?? ''), { abort: true })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const unknown = z
.object({ [key]: z.string().regex(/\S/) })
.refine((value) => !tags.has(value[key] ?? ''), { abort: true })
return z.union([known, unknown])
const unknown = z
.looseObject({ [key]: z.string().regex(/\S/) })
.refine((value) => !tags.has(value[key] ?? ''), { abort: true })
return z.union([known, unknown])

@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 found.

Reviewed changes

  • Never delete on read (journal-open.ts, journal-store-open.ts, journal-per-session-import.ts): the fold now returns { state, newer, damage } instead of a read-only/corrupt/truncate tuple. Damage (unparseable, misnumbered, gap, missing epoch) and a newer build's row both refuse the open with every row left in place; a newer row wins over damage beside it. failLoadOnUnloadableJournal is the single refusal point for the store open, the deferred per-chat copy, and the startup preview. The read-time repair (suffix deletion, repair marker, transcript rebuild, historyFilePath adapter hook) is gone.
  • Unknown content inside a known row stays writable (agent-session-journal-open-union.ts, agent-session-journal-schemas.ts, agent-session-journal-body-admission.ts): openDiscriminatedUnion wraps item bodies, blocks, goal states and approval subjects so an unknown non-whitespace tag reads as-is; an unknown lifecycle-mutation or sent-message kind is unreadable (refuses the load). .strict() removed from PromptOption/Question.
  • Write-time readback (journal-row-table.ts): insertJournalRow parses the serialized row with the reader replay uses and throws journal_row_rejected, rolling back the transaction.
  • It says why once (desktop NativeChatStructuredSession.tsx/native-chat-view-state.ts, mobile MobileNativeChatView.tsx/use-mobile-structured-agent-state.ts): a final read refusal (damage or journalWrittenByNewerOrca) takes the whole pane; the refusal rides the reducer so the phone can tell it apart.
  • Approvals and sends (agent-session-approval-subject.ts, NativeChatApprovalCard.tsx, MobileNativeChatPermission.tsx, use-structured-agent-session.ts, use-mobile-structured-send-with-outcome.ts): an unknown subject shows detail plus "This request needs a newer version of Orca.", disables answer buttons, keeps cancel; while all pending prompts are unanswerable the composer stays open and sends skip queue-if-active.
  • Restart offers (structured-agent-session-restart-*, agent-session-recovery-capsule.ts): createNewerOrcaChats skips newer-Orca chats in the listing but not at send (so their offer is kept, not spent), hides their filed failures, and "dismiss all" reveals every record first then clearAll(now, keep).
  • Startup restore (structured-agent-session-restart-restore.ts, structured-agent-session-host-tabs.ts): one chat's open failure is logged and the rest restore; the failed chat keeps its tab via markUnopened.

I read the full diff end-to-end (169 files), traced the fold/refusal precedence against the tests, and dispatched an independent review of the "an unknown kind is kept and drawn by nobody" guarantee across every production consumer of item bodies, blocks, goal states and approval subjects. Every such consumer guards on the discriminant or falls through to a skip default, so an unknown kind cannot throw or misrender.

I also assessed the two findings the existing bot review posted on this head and consider neither a live defect:

  • openDiscriminatedUnion's fallback strips non-tag fields from .parse()/.safeParse().data (src/shared/agent-session-journal-open-union.ts:28-31). True of z.object, but no production caller consumes the parsed output for these schemas — isAdmissibleAgentJournalItemBody and isAgentSessionRewindRecord only read .success, and restoreRewindJournalBody returns the raw stored body. Using z.looseObject would make the fallback match the "kept as-is" contract if a future caller ever consumes .data; it is not a bug today.
  • The reliability-gate command still names the deleted agent-session-journal-recovery.test.ts (config/reliability-gates.jsonc:654). The author kept it deliberately as the 2026-09-11 historical evidence run with an explanatory comment; retiring it is optional tidiness, not a correctness fix.

One latent hazard lives outside this PR's changed lines, noted only because this PR documents the open turn-state guarantee:

Technical details
# `agentTurnVerdict` / `agentTurnLifecycleText` switch an open string without a default

## Affected sites
- `src/shared/agent-turn-outcome.ts:50` — `switch (turn.state)` returns `'interruption' | 'unconfirmed' | null` and has no `default`; an unknown state falls out and the function returns `undefined`, violating its declared `AgentTurnOutcome | null`.
- `src/shared/agent-turn-lifecycle-text.ts:9` — same shape; returns `undefined` for an unknown state, and its result feeds `legacyAgentJournalTurnStatusBody(...).text`.

## Why it is latent, not live
The persisted schemas type turn `state` (and `turnLifecycle.state`) as the open `z.string().min(1)` (`agent-session-journal-schemas.ts:254,270`), while the TypeScript `AgentJournalTurnLifecycleState` is closed. No current consumer shows a visible break — every caller treats the result as falsy/absent. Not introduced by this PR.

## Required outcome
If the open-union guarantee should cover turn `state` too, give both helpers a `default` (`null` / `''`) so an unknown state behaves like the absent one.

## Suggested approach (optional)
Add `default: return null` to `agentTurnVerdict` and `default: return ''` to `agentTurnLifecycleText`; or narrow the schema to the closed lifecycle set if an unknown turn state should instead be damage.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

…unknown-content-read-only

# Conflicts:
#	src/renderer/src/components/native-chat/NativeChatStructuredSession.tsx
#	src/renderer/src/components/native-chat/use-structured-agent-session-delivery-notices.ts
#	src/renderer/src/components/native-chat/use-structured-agent-session.ts
…own, 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.

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

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟡 Minor · Retain offers when reveal cannot classify the… · structured-agent-session-restart-resume-host.ts:324-329

src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts:324-329
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Retain offers when reveal cannot classify the journal.

revealSession converts transient journal-open errors to openRefusal, but newerOrca.note deletes the session for every refusal except journalWrittenByNewerOrca. Dismiss all then passes savedByNewerOrca to clearAll, so an unclassified newer-format offer can be permanently removed.

Track reveal failures separately and include them only in the dismissal keep predicate. Do not include them in has, because this host must still list them when possible.

Suggested fix
 const newerOrca = createNewerOrcaChats(() => deps.journalDatabase.readOnly)
@@
     savedByNewerOrca: newerOrca.has,
+    keepOnDismiss: newerOrca.keep,
-        const keep = (marker: AgentSessionResumeMarker) => deps.savedByNewerOrca(marker.sessionId)
+        const keep = (marker: AgentSessionResumeMarker) => deps.keepOnDismiss(marker.sessionId)
 export function createNewerOrcaChats(databaseIsNewer: () => boolean) {
   const chats = new Set<string>()
+  const unclassified = new Set<string>()
   return {
     has: (sessionId: string): boolean => databaseIsNewer() || chats.has(sessionId),
+    keep: (sessionId: string): boolean =>
+      databaseIsNewer() || chats.has(sessionId) || unclassified.has(sessionId),
     note: (sessionId: string, revealed: { openRefusal?: AgentSessionWireRefusal } | null): void => {
       if (revealed?.openRefusal?.details?.reason === 'journalWrittenByNewerOrca') {
         chats.add(sessionId)
+        unclassified.delete(sessionId)
+      } else if (
+        revealed === null ||
+        revealed?.openRefusal?.details?.reason === 'journalUnavailable'
+      ) {
+        unclassified.add(sessionId)
       } else {
         chats.delete(sessionId)
+        unclassified.delete(sessionId)
       }
     }
   }
 }

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e4626f31-ca9d-4f17-bbcc-4a1e22480198
📥 Commits

Reviewing files that changed from the base of the PR and between 5504adc and 5d9f109.

📒 Files selected for processing (18)
  • mobile/src/session/MobileNativeChatView.test.ts
  • mobile/src/session/use-mobile-structured-agent-session.ts
  • src/main/codex/codex-structured-session-adapter.test.ts
  • src/main/native-chat/agent-session-journal/journal-store.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-resend-answer.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.ts
  • src/main/runtime/rpc/methods/structured-agent-session-at-rest.test.ts
  • src/main/runtime/rpc/methods/structured-chat-tab-table.test.ts
  • src/renderer/src/components/native-chat/NativeChatStructuredSession.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
💤 Files with no reviewable changes (2)
  • src/main/codex/codex-structured-session-adapter.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.ts
🚧 Files skipped from review as they are similar to previous changes (7)
  • src/renderer/src/i18n/locales/fr.json
  • src/renderer/src/i18n/en-runtime-required.json
  • src/renderer/src/i18n/locales/es.json
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/i18n/locales/ko.json
  • src/renderer/src/i18n/locales/zh.json
  • src/renderer/src/i18n/locales/ja.json

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

@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 found.

Reviewed changes

  • Merged origin/main (a154562) into the branch: adopted main's resend-answer operation ledger (#25158), the useStructuredAgentSessionDeliveryNotices hook, the cut-turn notice projection and TUI_AGENT_DISPLAY_NAMES, plus incidental imports in journal-store.ts and structured-agent-session-restart-continuation.ts.
  • Merge reconciliation verified at HEAD: the branch's final-read-failure gating survived — desktop selectNativeChatViewState(session, { readRetries: !readFailedFinally }) and mobile readFailedFinally are intact, and the controller still supplies every field the adopted delivery-notice hook now reads (queuedMessageIds, submissions, journalItems, failedHere, retry).
  • Resend-answer test updated: a resend against a read-only (newer Orca) store now asserts agent_session_operation_unknown / outcomeUnknown and that no operation row was written, replacing the pre-merge replay-success assertion.
  • Mobile view test strengthened: a final read failure renders no send-error banner beside its words, while a clearable failure still shows one.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@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 found.

Reviewed changes

  • Merged origin/main into the branch (47fa092f78, main at e46fa0df13): a main-only integration bringing the CSV table editor, provider credit balances, and Linear issue-identifier work. None of its 79 files are under native-chat, mobile/session, or the journal/approval shared modules, and git diff 47fa092f78^1 47fa092f78 matches the 5d9f109970..47fa092f78 diff exactly — the merge touched no branch file.
  • Locale merge verified additive: the six locales/*.json catalogs are the only files the merge shares with this PR. The merge adds only main's keys (CSV editor, provider credits), and this branch's new strings (chatSavedByNewerOrca, historyUnusable, needsNewerOrca, savedByNewerOrca, unsupported) are intact at HEAD in both locales/en.json and en-runtime-required.json.

The branch's own changes are untouched since the prior review, so there is no new behavioral surface to re-review.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@brennanb2025
brennanb2025 merged commit 98171a8 into main Oct 5, 2026
54 of 56 checks passed
brennanb2025 added a commit that referenced this pull request Oct 5, 2026
main #24576 deleted agent-session-journal-recovery.ts and its two tests; take the deletion
(this branch only re-pointed them at the neutral provider handle). Main's new journal and
cross-version tests build journal identities with codexProviderHandle(); the older-build open
in the newer-chat cross-version test keeps the legacy handle shape it reads.
brennanb2025 added a commit that referenced this pull request Oct 5, 2026
main #24576 removed historyFilePath from the structured adapter interface; drop this branch's
registry-routed copy in the adapter router. Main's new host and turn tests
(newer-content-host, unopened-tab, stop-note-withdrawn-send) pass the agent registry this branch
made required.
brennanb2025 added a commit that referenced this pull request Oct 5, 2026
Restart offers (main #24576 newer-Orca chats + this branch's per-client audience):
- candidates: the offer reader takes both the agent registry and savedByNewerOrca.
- failure ledger dismiss: keeps the audience parameter; beforeClearAll may be async (main) and
  receives the audience; an unnamed audience dismissal also keeps a newer Orca's records, as an
  unnamed local one already does.
- resume host: deps are a Pick of the host deps plus journalDatabase; dismiss clears the
  witnesses for the audience and reveals every record's chat before an unnamed dismissal.
The merged resume host crossed max-lines, so the action result's refusal fill-in and row refresh
move unchanged to structured-agent-session-restart-action-result.ts.
brennanb2025 added a commit that referenced this pull request Oct 5, 2026
- journal-store.ts imports: main #24576 dropped journalStoreLoadedFields; this branch types
  appendResolvedItem off JournalItemAppender, so neither it nor JournalResolvedItem is imported.
- event-sink-queue: keeps this branch's journalItems; journalStopDecidesTurn takes main #24864's
  two arguments (openedBy plumbing removed on main).
brennanb2025 added a commit that referenced this pull request Oct 5, 2026
agent-session-journal-schemas.ts: main #24576 (through C1a/C1b) made the known body union internal
(KnownItemBody) behind an open union; this branch's TurnLifecycleFields, tool-call endedAs and the
shared turn fields sit on it unchanged.
brennanb2025 added a commit that referenced this pull request Oct 5, 2026
…-history

Main's #24576 removed journal repair, the rebuild from provider history and the
read-only latch: a damaged chat, or one a newer Orca saved, now fails to load
and nothing is deleted. This merge takes main's side throughout:

- The PR's "load is corrupt, a rebuild is owed" bit is gone, with its status
  writes on the repair path; journal-repair-marker.ts and
  agent-session-journal-recovery.ts are deleted as on main.
- The status derivation always counts live rosters now, so
  JOURNAL_SESSION_STATUS_RULES goes to 3 with a corpus case for a roster under
  a former repair notice (the other 21 cases derive the rules-2 digest).
- An open that fails on its rows (damage or a newer build's) drops the chat's
  stored status in the same step, so startup never selects it again; the
  catch-up writes no row for such a chat, and startup does not settle a listed
  chat again after its open dropped the row.
- The PR's persisted tab listing already keeps a tab whose chat cannot open, so
  main's unopened-tab set is not taken; main's test now lists visible tabs.
- The rest-test rig counts opens at the journal store's open, since the open
  no longer asks the adapter for a history file.
brennanb2025 added a commit that referenced this pull request Oct 5, 2026
…a rollback breaks nothing

The status table arrived with schema version 5 so that an older build would open
the database read-only and never append history behind a status it does not
update. Main's #24576 refuses every chat in a database of a newer version, so a
rollback to a build with #24576 but not this change would have opened no chat at
all.

The table is now created at every writable open with no version bump, as the
queued-messages table is, and the database stays at version 4: an older build
opens it writable and ignores the table. Each row records the history tip it was
derived from (epoch, sequence and timestamp of the newest row, read inside the
same statement as the write), and every reader (the startup seed, the catch-up,
the per-chat-file restore, settle selection and the open's has-row check) treats
a row whose tip is not the chat's tip now as missing. So a row an older build
wrote history past, or a same-epoch repair regrew to the same sequence, is
derived again; a row whose chat is gone or moved is never selected to settle.
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