Repository navigation
fix(native-chat): never delete chat history on read; a chat Orca can't load says why once - #24576
Conversation
…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.
…e read-only overlay test
…unknown-content-read-only
…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.
…crolls back through it
…rovider never answered
- 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.
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:
What changes for you, running an older Orca on a chat a newer Orca wrote:
What the review changed (the PR was largely reworked):
Deferred (follow-ups):
Verified:
Not verified:
|
…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
…, and keeps a newer Orca's hidden ones
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.
How it got here. The PR was largely reworked during review, after the common pattern was checked against the design.
Deferred / follow-ups.
Verified.
Not verified.
|
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
628dd6fb-777b-4c75-a8a9-c2834db1f1a5
📒 Files selected for processing (166)
config/reliability-gates.jsoncmobile/src/session/MobileNativeChatOverlay.tsxmobile/src/session/MobileNativeChatPermission.test.tsmobile/src/session/MobileNativeChatPermission.tsxmobile/src/session/MobileNativeChatView.test.tsmobile/src/session/MobileNativeChatView.tsxmobile/src/session/mobile-structured-agent-session-rpc.tsmobile/src/session/mobile-structured-agent-session-thrown-refusal.test.tsmobile/src/session/use-mobile-native-chat-session.tsmobile/src/session/use-mobile-structured-agent-session-newer-approval-send.test.tsxmobile/src/session/use-mobile-structured-agent-session-read-failure.test.tsxmobile/src/session/use-mobile-structured-agent-session.tsmobile/src/session/use-mobile-structured-agent-state.tsmobile/src/session/use-mobile-structured-send-with-outcome.tssrc/main/agent-hooks/first-work-branch-rename.test.tssrc/main/claude/claude-background-wake-retention.test.tssrc/main/claude/claude-structured-prompt-items.tssrc/main/codex/codex-close-turn-end.test.tssrc/main/codex/codex-requested-close-turn-timing.test.tssrc/main/codex/codex-structured-session-acquire.tssrc/main/codex/codex-structured-session-adapter-lifecycle.test.tssrc/main/codex/codex-structured-session-adapter.test.tssrc/main/codex/codex-structured-session-adapter.tssrc/main/codex/codex-structured-session-close.test.tssrc/main/codex/codex-structured-session-options-catalog.test.tssrc/main/codex/codex-structured-session-options.test.tssrc/main/codex/codex-structured-session-state.tssrc/main/codex/codex-structured-thread-facts.tssrc/main/codex/codex-structured-thread-open.test.tssrc/main/codex/codex-structured-thread-open.tssrc/main/native-chat/agent-session-journal/journal-approval-subject-kinds.test.tssrc/main/native-chat/agent-session-journal/journal-closed-sets.test.tssrc/main/native-chat/agent-session-journal/journal-corruption-repair.test.tssrc/main/native-chat/agent-session-journal/journal-cursor.tssrc/main/native-chat/agent-session-journal/journal-damage.test.tssrc/main/native-chat/agent-session-journal/journal-database-schema.tssrc/main/native-chat/agent-session-journal/journal-database.test.tssrc/main/native-chat/agent-session-journal/journal-epoch-controller.tssrc/main/native-chat/agent-session-journal/journal-epoch-replacement.tssrc/main/native-chat/agent-session-journal/journal-epoch-rollover.tssrc/main/native-chat/agent-session-journal/journal-file-format-remnant.test.tssrc/main/native-chat/agent-session-journal/journal-host-database-test-support.tssrc/main/native-chat/agent-session-journal/journal-legacy-import.test.tssrc/main/native-chat/agent-session-journal/journal-legacy-import.tssrc/main/native-chat/agent-session-journal/journal-open-failure.test.tssrc/main/native-chat/agent-session-journal/journal-open-failure.tssrc/main/native-chat/agent-session-journal/journal-open.tssrc/main/native-chat/agent-session-journal/journal-per-session-import.test.tssrc/main/native-chat/agent-session-journal/journal-per-session-import.tssrc/main/native-chat/agent-session-journal/journal-per-session-source.tssrc/main/native-chat/agent-session-journal/journal-prompt-body-bounds.tssrc/main/native-chat/agent-session-journal/journal-repair-disclosure.tssrc/main/native-chat/agent-session-journal/journal-repair-marker.tssrc/main/native-chat/agent-session-journal/journal-row-context-usage.test.tssrc/main/native-chat/agent-session-journal/journal-row-schema.test.tssrc/main/native-chat/agent-session-journal/journal-row-schema.tssrc/main/native-chat/agent-session-journal/journal-row-table.tssrc/main/native-chat/agent-session-journal/journal-stop-and-resume-rows.test.tssrc/main/native-chat/agent-session-journal/journal-store-collaborators.tssrc/main/native-chat/agent-session-journal/journal-store-open.tssrc/main/native-chat/agent-session-journal/journal-store-restore.tssrc/main/native-chat/agent-session-journal/journal-store-schema.test.tssrc/main/native-chat/agent-session-journal/journal-store.test.tssrc/main/native-chat/agent-session-journal/journal-store.tssrc/main/native-chat/agent-session-journal/journal-streaming-replay.test.tssrc/main/native-chat/agent-session-journal/journal-unknown-content.test.tssrc/main/native-chat/agent-session-journal/journal-unknown-row-kind.test.tssrc/main/native-chat/agent-session-journal/journal-write-guards.tssrc/main/native-chat/agent-session-journal/queued-message-bookkeeping-failure.test.tssrc/main/native-chat/agent-session-journal/queued-message-store.test.tssrc/main/native-chat/agent-session-wire/agent-session-history-page.tssrc/main/native-chat/agent-session-wire/agent-session-journal-recovery.test.tssrc/main/native-chat/agent-session-wire/agent-session-journal-recovery.tssrc/main/native-chat/agent-session-wire/agent-session-subscriber-catch-up.tssrc/main/native-chat/agent-session-wire/structured-agent-session-acquisition-options.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-adapter-router.tssrc/main/native-chat/agent-session-wire/structured-agent-session-adapter.tssrc/main/native-chat/agent-session-wire/structured-agent-session-attach-context.tssrc/main/native-chat/agent-session-wire/structured-agent-session-attach-test-conversation.tssrc/main/native-chat/agent-session-wire/structured-agent-session-command-publication.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-conversation-open.tssrc/main/native-chat/agent-session-wire/structured-agent-session-forget-status.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host-delivery.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host-tabs.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host-types.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host.tssrc/main/native-chat/agent-session-wire/structured-agent-session-newer-content-host.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-prompt-cancel.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.tssrc/main/native-chat/agent-session-wire/structured-agent-session-queued-newer-orca.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-queued-wiring.tssrc/main/native-chat/agent-session-wire/structured-agent-session-read-restore.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-readable-restorer.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-refusal-retry.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-candidates.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-failure-ledger.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-newer-orca.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-offer-records.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-restore.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-restore.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-set.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-test-harness.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-wiring.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-resume.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-reveal.tssrc/main/native-chat/agent-session-wire/structured-agent-session-rewind.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-rewind.tssrc/main/native-chat/agent-session-wire/structured-agent-session-settled-attach-retry.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-startup-reconcile-failure.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-status-feed.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-status-feed.tssrc/main/native-chat/agent-session-wire/structured-agent-session-status-journal-projection.tssrc/main/native-chat/agent-session-wire/structured-agent-session-subscribers.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-subscribers.tssrc/main/native-chat/agent-session-wire/structured-agent-session-surface-lifetime.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-turn-completion-feed.tssrc/main/native-chat/agent-session-wire/structured-agent-session-unopened-tab.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-working-at-teardown.tssrc/main/native-chat/agent-session-wire/structured-rewind-journal-body.test.tssrc/main/native-chat/agent-session-wire/structured-rewind-journal-body.tssrc/main/native-chat/agent-session-wire/structured-rewind-retained-host-rows.test.tssrc/main/native-chat/agent-session-wire/structured-rewind-retained-host-rows.tssrc/main/runtime/agent-session-recovery-capsule-entries.tssrc/main/runtime/agent-session-recovery-capsule.tssrc/main/runtime/rpc/methods/structured-agent-session-at-rest.test.tssrc/main/runtime/rpc/methods/structured-agent-session-reveal.tssrc/main/runtime/rpc/methods/structured-agent-session-rpc.test-fixture.tssrc/main/runtime/rpc/methods/structured-chat-tab-table.test.tssrc/main/runtime/structured-agent-session-failure-logging.test.tssrc/main/runtime/structured-agent-session-integration-replay.test.tssrc/main/runtime/structured-agent-session-startup-tab-restore.test.tssrc/renderer/src/components/native-chat/NativeChatApprovalCard.test.tsxsrc/renderer/src/components/native-chat/NativeChatApprovalCard.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.final-read-failure.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.newer-approval.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.read-error.test.tsxsrc/renderer/src/components/native-chat/NativeChatStructuredSession.tsxsrc/renderer/src/components/native-chat/agent-session-write-notice-text.tssrc/renderer/src/components/native-chat/native-chat-view-state.tssrc/renderer/src/components/native-chat/structured-agent-session-read-transport.test.tssrc/renderer/src/components/native-chat/use-structured-agent-session-delivery-notices.tssrc/renderer/src/components/native-chat/use-structured-agent-session.queued-gating.test.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.tssrc/renderer/src/i18n/en-runtime-required.jsonsrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.jsonsrc/shared/agent-session-approval-subject.test.tssrc/shared/agent-session-approval-subject.tssrc/shared/agent-session-journal-body-admission.test.tssrc/shared/agent-session-journal-body-admission.tssrc/shared/agent-session-journal-open-union.tssrc/shared/agent-session-journal-schemas.tssrc/shared/agent-session-journal-types.tssrc/shared/agent-session-refusal-notice.test.tssrc/shared/agent-session-refusal-notice.tssrc/shared/agent-session-refusal-reason-words.tssrc/shared/agent-session-write-notice-copy.tssrc/shared/structured-agent-session-read-refusal.tssrc/shared/structured-agent-session-unknown-item.test.tstests/e2e/cross-version-wire/agent-session-newer-chat-refusal.unit.test.tstests/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. |
There was a problem hiding this comment.
📐 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' srcRepository: 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 -200Repository: 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.mjsRepository: 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' || trueRepository: 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",| const unknown = z | ||
| .object({ [key]: z.string().regex(/\S/) }) | ||
| .refine((value) => !tags.has(value[key] ?? ''), { abort: true }) | ||
| return z.union([known, unknown]) |
There was a problem hiding this comment.
🗄️ 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 -80Repository: 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 -30Repository: 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 -160Repository: 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.
| 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]) |
There was a problem hiding this comment.
✅ 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.failLoadOnUnloadableJournalis 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,historyFilePathadapter 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):openDiscriminatedUnionwraps 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 fromPromptOption/Question. - Write-time readback (
journal-row-table.ts):insertJournalRowparses the serialized row with the reader replay uses and throwsjournal_row_rejected, rolling back the transaction. - It says why once (desktop
NativeChatStructuredSession.tsx/native-chat-view-state.ts, mobileMobileNativeChatView.tsx/use-mobile-structured-agent-state.ts): a final read refusal (damage orjournalWrittenByNewerOrca) 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 skipqueue-if-active. - Restart offers (
structured-agent-session-restart-*,agent-session-recovery-capsule.ts):createNewerOrcaChatsskips 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 thenclearAll(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 viamarkUnopened.
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 ofz.object, but no production caller consumes the parsed output for these schemas —isAdmissibleAgentJournalItemBodyandisAgentSessionRewindRecordonly read.success, andrestoreRewindJournalBodyreturns the raw stored body. Usingz.looseObjectwould 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.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.
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟡 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 winRetain offers when reveal cannot classify the journal.
revealSessionconverts transient journal-open errors toopenRefusal, butnewerOrca.notedeletes the session for every refusal exceptjournalWrittenByNewerOrca.Dismiss allthen passessavedByNewerOrcatoclearAll, 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
📒 Files selected for processing (18)
mobile/src/session/MobileNativeChatView.test.tsmobile/src/session/use-mobile-structured-agent-session.tssrc/main/codex/codex-structured-session-adapter.test.tssrc/main/native-chat/agent-session-journal/journal-store.tssrc/main/native-chat/agent-session-wire/structured-agent-session-queued-messages.tssrc/main/native-chat/agent-session-wire/structured-agent-session-resend-answer.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.tssrc/main/runtime/rpc/methods/structured-agent-session-at-rest.test.tssrc/main/runtime/rpc/methods/structured-chat-tab-table.test.tssrc/renderer/src/components/native-chat/NativeChatStructuredSession.tsxsrc/renderer/src/components/native-chat/use-structured-agent-session.tssrc/renderer/src/i18n/en-runtime-required.jsonsrc/renderer/src/i18n/locales/en.jsonsrc/renderer/src/i18n/locales/es.jsonsrc/renderer/src/i18n/locales/fr.jsonsrc/renderer/src/i18n/locales/ja.jsonsrc/renderer/src/i18n/locales/ko.jsonsrc/renderer/src/i18n/locales/zh.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.
…unknown-content-read-only
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Merged
origin/main(a154562) into the branch: adopted main's resend-answer operation ledger (#25158), theuseStructuredAgentSessionDeliveryNoticeshook, the cut-turn notice projection andTUI_AGENT_DISPLAY_NAMES, plus incidental imports injournal-store.tsandstructured-agent-session-restart-continuation.ts. - Merge reconciliation verified at HEAD: the branch's final-read-failure gating survived — desktop
selectNativeChatViewState(session, { readRetries: !readFailedFinally })and mobilereadFailedFinallyare 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/outcomeUnknownand 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.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Merged
origin/maininto the branch (47fa092f78, main ate46fa0df13): a main-only integration bringing the CSV table editor, provider credit balances, and Linear issue-identifier work. None of its 79 files are undernative-chat,mobile/session, or the journal/approval shared modules, andgit diff 47fa092f78^1 47fa092f78matches the5d9f109970..47fa092f78diff exactly — the merge touched no branch file. - Locale merge verified additive: the six
locales/*.jsoncatalogs 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 bothlocales/en.jsonanden-runtime-required.json.
The branch's own changes are untouched since the prior review, so there is no new behavioral surface to re-review.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
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.
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.
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.
- 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).
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.
…-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.
…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.

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:
After this change, opening a chat never deletes anything:
What Changed
What you see
Before, opening a chat:
After:
Mechanism
src/shared/agent-session-journal-schemas.ts). A body'skind, a block'stype, a goal'sstateand an approval subject'skindare 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, andjournal-closed-sets.test.tsfails if one changes without it.journal-approval-subject-kinds.test.tsfails if an approval subject kind is added at all before clients can be told apart (STA-9262).journal-open.ts,failLoadOnUnloadableJournalinjournal-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 existingagent_session_journal_unreadable/journalWrittenByNewerOrcareason. 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.agent-session-refusal-reason-words.ts, the reason table moved out ofagent-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."isFinalAgentSessionReadRefusalcounts it as final, as damage is, so the pane stops retrying.NativeChatStructuredSession.tsxlets a final read failure win over messages inselectNativeChatViewState(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 reportsreadFailedFinally, andMobileNativeChatView.tsxrenders only the existing failed-load surface. A failure that can still clear keeps today's view.insertJournalRowinjournal-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 throwsjournal_row_rejectedand rolls the write back. A per-chat file's verbatim copy is checked by folding it as it is copied.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.readOnlyfield and the reconnect without a cursor that read it, the locked composer and its placeholder, and the greyed-out controls. Thejournal_repairstable stays in the schema, unused here, because older builds on the same schema version still read it.structured-agent-session-restart-restore.ts) catches each chat's failure, logs it for that chat, and carries on.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.revealpublishes 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.restartResumableDismisswith 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 (clearAllwith a keep predicate, still fencing late writers).agent-session-approval-subject.ts, used byNativeChatApprovalCard.tsxandMobileNativeChatPermission.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.pendingPromptsAllUnanswerableHere): desktop keeps the composer, and desktop and phone send withoutdelivery: '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.Mixed versions
createJournalOpenReadRefusals).agent-session-newer-chat-refusal.unit.test.tsfolds this host's refusal through the released readers and checks that neither calls it damage.readOnlypage field this PR first added was never released, so removing it reaches no client.structured-agent-session-resend-answer.test.ts,MobileNativeChatView.test.ts).Why
Differences from the common pattern
journal-approval-subject-kinds.test.tsfails 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):
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):journal-unknown-row-kind.test.ts,journal-unknown-content.test.ts,journal-store-schema.test.ts,journal-damage.test.ts,journal-streaming-replay.test.tsandjournal-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 withjournalWrittenByNewerOrcaand 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.tsandstructured-chat-tab-table.test.ts: over a newer Orca's records every chat tab is listed, nothing is written, and no chat opens.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 withjournalCorrupt; "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.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.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.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).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) andstructured-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.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.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.tsxandMobileNativeChatPermission.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.tsandstructured-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.tsandstructured-agent-session-rewind.test.ts.structured-agent-session-restart-restore.test.ts(+1): one chat whose open throws is logged and the other eleven open.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, mobiletscand 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.
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)