Repository navigation
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
Conversation
…nd it does not know
…rites, rewinds and reopens
…d-only unless its writer declared it skippable
A row of a kind this build does not know, in a well-formed envelope, now latches the chat
read-only with every row kept, the same way a newer row version does. It is read past only
when its writer declared `ifUnknown` on the row: `skip` (a rewind drops it) or `carry` (a
rewind carries it after the rebuilt history, epoch, seq and fence restamped). Every existing
kind changes queue or turn state, so skipping by default would let an older build write from
a wrong fold.
- journal-row-kind-compatibility.ts: each kind states how older builds read it, typed over
every row kind, so a new kind cannot be added without a declaration.
- Rewind restates the Resume and Stop as before, then carries `carry` rows in source order;
the restatement goes back to { lifted, liveStop }.
- Replay treats a row whose body names another sequence than its stored key as malformed at
the key, so the next write never collides with it; catch-up reads stop there too.
… kind only latches read-only An older Orca now treats a row of a kind it does not know exactly like a row from a newer schema version: every row stays on disk and the chat opens read-only until an update. The writer-declared skip/carry opt-in, its in-memory placeholder, the carry through rewinds and the per-kind registry are removed: no current or planned kind could use them, and they can come with the first kind that may safely be read past. Kept: an unknown kind needs the envelope every row keeps (epoch, sequence, fence, timestamp), else it is damage as before; a row whose body names another sequence than its stored key is malformed at the key; the epoch row's validator names its kind. The schema header states the rule for adding a kind: keep the envelope, and either ship the reader first or bump `v`.
Review statusThe problem. When an older Orca opened a chat that a newer Orca had written, it deleted everything from the first row of a kind it didn't recognise. It treated that row as damage, removed it and every row after it, ordinary messages included, then rebuilt what it could from the agent's own transcript. Anything only Orca recorded (sends, Stops, Resumes) was gone for good, even after upgrading again. Because of this, no release could safely add a new kind of history row. That is why the Stop event and the Resume marker currently ride on removal rows with an extra field. What changes for users. An older Orca that meets a row kind it doesn't know now keeps every row and opens the chat read-only. You can still see the history up to that row. Sending is refused with "Chats were saved by a newer Orca. Update Orca to keep using them.", and the chat comes back whole after you update. This is how Orca already handles a row from a newer format version. The trade-off: before, the chat was usable again right away at the cost of history; now nothing is lost, but the chat is frozen until you update. No current release writes an unknown kind, so nobody sees a difference until a later release adds one. A second repair is fixed too. Each row is stored under a number and also carries that number in its own text. When the two disagreed, opening the chat trusted the text, which either deleted a valid row and left a hole in the numbering, or left a row that made every later write fail. Now the row counts as damaged at its stored number: it is removed with the usual "could not be read" notice, and writing works again. What the review changed (the design changed twice, so the final diff is much smaller than the original):
Deferred (not fixed here):
Verified (head 7704a6b, CI green):
Not verified: no app run or live QA. This is a host-only change with no new UI, and the read-only state is the existing one a newer format version triggers. Live QA was skipped by agreement. |
…row union Each kind's own-field check now lives in one table keyed by every kind JournalRow holds, and the set of kinds this build knows is derived from that table. A kind added to the union without a check fails to compile, rather than latching this build's own chats read-only as a newer build's kind. A test reads one valid row of every kind.
|
There was a problem hiding this comment.
ℹ️ No critical issues — one minor documentation nit.
Reviewed changes
- Unknown journal row kinds now fail closed to read-only instead of deleting history.
parseJournalRowreports an unknown non-emptykindthat carries the shared row envelope (epoch/seq/fence/ts) as unreadable, so an older build keeps every row and refuses writes until it is updated, matching the existing newer-vpath.journal-open.tsthen latchesreadOnlywithtruncateFromomitted, so nothing is deleted; the user-facing send refusal is unchanged becauseisJournalWrittenByNewerOrcakeys offjournal_read_only(journal-open-failure.ts:66). - A row whose body
seqdisagrees with its stored key is now damage at the key.startJournalRowFoldtruncates from the stored sequence instead of trusting the body's number, fixing a valid earlier row being deleted (inner lower) and every later write failing on a duplicate number (inner higher). Only rowsparseJournalRowaccepts are subject to the check, so a newer kind stays read-only first. - Tests. A new 16-test unit file covers the parse cases, read-only open/write-refusal/reopen/upgrade, and the seq-mismatch repair; a new cross-version test pins the pre-change baseline still deleting an unknown kind while this build keeps it.
- Comments/types in
journal-row-schema.ts,agent-session-journal-types.ts, andjournal-open.tsnow describe the envelope rule a new kind must keep.
I ran pnpm test src/main/native-chat/agent-session-journal (37 files / 400 tests) and the new file plus journal-row-schema.test.ts (39 tests); all pass. The readOnly block on writes was confirmed through assertJournalWritable in journal-row-writer.ts, journal-queued-messages.ts, and journal-epoch-controller.ts.
ℹ️ Nitpicks
src/main/native-chat/agent-session-journal/journal-write-guards.ts:21and thejournal_read_onlymessage at line 27 still say the journal "uses a newer schema", butreadOnlynow also latches on an unknown row kind. Only the developer log line is affected — the user-facing refusal is keyed offjournal_read_onlyand reads correctly — so widening the wording can wait for the next touch.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (5)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review. 📝 WalkthroughWalkthroughThe journal parser now classifies unknown row kinds with a valid envelope as unreadable. Journal loading preserves unreadable rows, sets the journal read-only, and does not count them as malformed. The loader treats malformed rows and embedded-sequence mismatches as truncation points. Tests cover parsing, reopening, repair, append rejection, and cross-version behavior. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to An older Orca now keeps a chat containing a newer row kind read-only and preserves its history instead of deleting it. No merge-blocking issues were found. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change favors preserving conversation history over deleting data an older build cannot understand. Inspected write and recovery paths respect read-only mode. No introduced security vulnerability was established, but concurrent-handle behavior and some identity checks remain unverified. Retained concerns Security review detailsSecurity Blast Radius
Trust Boundaries and Controls
Resilience and Maintainability Implications
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes (delta since f30c080b)
- Known row kinds are now derived from the row union.
journal-row-schema.tsreplaces the hardcodedROW_KINDSset withROW_FIELD_CHECK_BY_KIND: Record<JournalRow['kind'], …>andKNOWN_ROW_KINDS = new Map(Object.entries(…)).isJournalRowlooks up the per-kind check and gates on the envelope, andparseJournalRowreuses the same map for the unknown-kind test. The old explicit kind branches moved into the table unchanged, so behavior is preserved; a kind added to the union without a check now fails to compile instead of silently reading as a newer build's kind. - A guard test pins every kind.
journal-unknown-row-kind.test.tsaddsrowOfEveryKind(), typed as a mapped type overJournalRow['kind'], and asserts each parses as{ ok: true, row }.
I re-ran pnpm test src/main/native-chat/agent-session-journal (37 files / 401 tests) and pnpm tc:node, both clean at 7704a6b3.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

ELI5
Every native chat keeps its history as a numbered list of rows, and each row has a kind ("an item", "a send", "a removal", and so on). Until now, when Orca opened a chat and found a row of a kind it had never heard of, it decided the history was damaged from that row on. It deleted that row and every row after it.
That is what happens when you run an older Orca on a chat a newer Orca wrote, if the newer Orca added a new kind of row. The older Orca throws away the end of your conversation, including ordinary messages written after the new row, then rebuilds what it can from the agent's own transcript. Anything only Orca knew (sends, Stops and Resumes) is gone for good, even after you upgrade again.
After this change, an older Orca treats an unknown kind of row the way it already treats a row from a newer format version. It keeps every row on disk and opens the chat read-only until you update. It also fixes a second way history was lost: a row whose own stored number disagrees with the number it is filed under is now treated as damage at that row, where before Orca either deleted a valid row and left a hole in the numbering, or made every later write fail.
What Changed
Before, an older Orca opening a chat that a newer Orca had written a new kind of row into:
After, the same Orca:
Be aware of the trade-off. Before, the chat was usable again right away, at the cost of history. Now it is frozen until you update, with nothing lost. This is the same thing a row from a newer format version already does today: Orca's
turnitem shipped that way. Today's Orca writes no unknown kinds, so the difference first shows up on a downgrade after some later release adds one.A row is still damage, repaired exactly as before, when:
A second repair is fixed. Each row is stored under its number and also carries that number inside its text. When the two disagreed, opening the chat trusted the number inside the text. I confirmed main's behaviour by running the new tests with only this check reverted:
Now the row is treated as damaged at its stored number. It is removed with the usual "could not be read" notice, the rows before it stay, and the next write succeeds. A row of an unknown kind is never checked this way: like a newer-format row, it is kept and the chat goes read-only.
Mechanism
journal-row-schema.ts. Reading a row checks its format version first; a newer version is unreadable, as before. Then, if the kind is one this build does not know, and the row has the fields every row keeps, it is unreadable too: the existing read-only outcome. The fields are a non-emptyepoch(which history of the chat it belongs to), an integerseqof at least 1 (its number), an integerfence(which Orca process wrote it) and a numericts. Without them it is still malformed, as is a known kind with broken fields.journal-open.ts. When a chat opens, a row whose inner number differs from its stored number counts as malformed at the stored number. An unreadable row still stops the read with nothing deleted. The rest, read-only state included, is the existing path, unchanged.journal-row-schema.ts. Builds from before this change still delete from an unknown kind. So a new kind must:Follow-up, not in this PR
The Stop event (#24072, #24311) and the Resume marker still ride on a removal row with an extra field, which every Orca ignores. A Stop changes the queue and how a turn ends. An older Orca that read past it would release messages you stopped and record your cancelled turn as a failure. It can get a kind of its own in one of two ways:
The cross-version test pins the current boundary: the writable baseline build still deletes an unknown kind.
Why
The alternatives:
Differences from the common pattern
The common pattern: a reader that meets a record type it does not know fails closed and never deletes the record. This change brings Orca in line with it.
Visual Proof
N/A: no UI change. The read-only state is the existing one a newer format version already shows.
Testing
src/main/native-chat/agent-session-journal/journal-unknown-row-kind.test.ts(17 tests):journal_read_only), and after a reopen every stored row is byte-identical.tests/e2e/cross-version-wire/agent-session-stop-event-downgrade.unit.test.ts). For a newer kind, this build opens read-only, refuses the write and keeps every row byte-identical. The pinned writable baseline build, which predates this change, deletes it on open. The two existing Stop/Resume downgrade tests are unchanged.tombstone) missing from the known-kind tableoxlinton the changed files and the changed-code quality gate are clean. The scopedtc:nodetypecheck passes.