Repository navigation
fix(native-chat): a failed startup chat-lease save no longer puts the app into "Session restore failed" - #23964
Conversation
…ailing app startup At startup the chat host re-checks every saved chat's lease and writes the result to agent-sessions.json. If that write failed (the file lock gave up, the file could not be written, or the file was written by a newer Orca and is read-only here), reconcileRestartLeases rejected, the startup IPC call rejected, and the renderer fell into its degraded "Session restore failed. Changes won't be saved until restart" mode. The reconcile is bookkeeping: a lease left unreconciled grants no writer, and every attach, send and read of a chat reconciles its own lease again. So the startup reconcile now reports its failure through a new optional host dependency, onStartupReconcileFailure, and resolves. The runtime routes it to its onError sink under the scope structured-agent-session-startup-reconcile, or logs it when no sink is installed (the desktop installs none).
…eping With native chat on and a chat tab open at quit, the renderer's startup also awaits the chat tab restore (session.tabs.listAll). That restore re-ran the lease reconcile before reading each chat and rethrew its store failure, then recorded each restored tab as visible through a store transaction that throws on a held lock or a read-only store. Either one failed the restore, so startup still fell into "Session restore failed". Reading a chat grants no writer, so the reconcile startup and the restore run is now a reader's: createReaderReconcile never throws, answers whether every lease is settled (recovery is resolved only then; the journal opens either way), and reports each distinct failure once until a reconcile settles. Attach and agent start keep the strict reconcile. The restore's tab republish logs a failed visibility write and still publishes the tab, since a client drops every unpublished chat tab; user-driven publishes still refuse. The host dependency is renamed onLeaseReconcileFailure (scope structured-agent-session-lease-reconcile), since it now also reports for reads.
…read path Round-2 review found two more writes on the startup chat restore that could still fail it and put the app into "Session restore failed": republishing a /clear replacement recorded its tab visibility strictly, and resolving a chat's recovery rethrew its store error. The restore also paid one lock wait per tab and per batch of chats while the lock stayed held. The restore now derives tabs from state it already holds: - publishStructuredAgentSessionTab splits into the strict write and projectStructuredAgentSessionTab, which only updates the runtime's snapshot. The restore and /clear replacements only project: a saved tab index already lists every restored chat, and a /clear moves the tab in the same write that commits it. visibilityWriteMayFail is gone. - Chats a legacy profile restores that the index does not list are recorded in one best-effort transaction (store.showSessionTabs), so a failure leaves the index absent to seed again rather than partial. - The read restore's recovery resolution is caught and reported through onLeaseReconcileFailure, deduplicated with the reconcile's reports. - Once lease bookkeeping fails in a restore pass, the rest of that pass skips it, so a held lock costs one wait for the startup reconcile and one for the restore, however many chats are open. User actions (create, reveal, attach, send, the /clear commit) keep their strict writes.
…op its bookkeeping at the first failure The restore now runs one reader lease check for the pass and lets each chat re-check and resolve recovery only while the pass is still settled. The first refusal or failed write clears it for the rest of the pass, and every chat is still opened for reading. With another process holding the lock, startup waits on it once in prepare and once in the restore, however many chats are open; a legacy profile waits once more for its tab-index seed.
…e per-write gate This PR had stopped startup waiting for the lease check (the pass that settles each chat's previous-run owner) and instead made every write to an unchecked chat wait for it. After #23964 that check can no longer fail startup, and a review run measured it at about 30 ms on a 247-chat Mac profile, so the gate bought nothing but a second mechanism. It also reordered two writes that arrived while a chat's lease flipped from unchecked to checked. Startup's structured step, the write path and worker release are back to the base's shape: startup awaits the non-throwing check, writes enter the chat's queue in arrival order, and an agent start still runs its own strict check before it acts. What stays is the listing from records and the tab table, and the chat history restore that starts after the list has answered.
Brings in main at 75040eb. main's record-store test harness moved the host test harness's store from <root>/store to <root>/agent-sessions, so the startup reconcile failure test now opens, edits and reads the relaunched store through agent-session-record-store-test-harness instead of the old path.
|
Review status: ready for review (head 871f64b, updated after round 5). The problem. At startup Orca checks the chat records file ( What changes for the user. When that bookkeeping write fails, Orca starts normally. There is no toast, open chat tabs come back with their history, and other changes save as usual. The failure is logged, and the records file is left untouched. Actions the user takes (sending, attaching, /clear) still report a failed write, because those can't succeed without it. With a lock held by another process, startup waits about one lock timeout (~3 s) per startup step no matter how many chats are open, where earlier drafts waited once per chat. Fixed during review (4 rounds plus an architecture challenge; details in the PR body):
Deferred (not in this PR):
Verified:
Not verified:
|
|
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Warning Review limit reachedNext included review available in 16 minutes. View limit detailsLimit details: You’ve used all 10 included reviews currently available. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. Review configuration: ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (18)
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 (2)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review. 📝 WalkthroughWalkthroughLease reconciliation failures are now reported while startup and readable-session restoration continue. Restart restoration skips later lease-settlement attempts after a failure but continues restoring session journals. Startup tab restoration selects saved or profile session IDs, seeds missing tab-index entries, and projects restored and replacement tabs. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to Startup can continue restoring readable chats when record-store bookkeeping fails. No concrete merge-blocking risk remains. Architecture SummaryArchitecture risk: 🔵 Low · up to The change affects 1 system. Changed systems: Architecture concerns Review detailsSystems and components
Before / after behavior
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
Full details: Description checkExplanation The description is detailed and covers the user impact, implementation, rationale, testing, visual proof, scope, and known limitations. However, it does not provide the required linked issue, and the full lint, typecheck, test, and build checklist is not confirmed. ✨ Finishing Touches 💡 2📝 Generate docstrings 💡
🛠️ Fix failing CI checks 💡
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
- Reader lease bookkeeping can no longer fail a read.
createReaderReconcilenever throws andreconcileRestartLeasesnow delegates to it, reporting each distinct failure once (reportEachFailureOnce, keyed by errorcode/message) throughonLeaseReconcileFailureorconsole.warn. - One bounded reconcile per restore pass. The restore now runs one pass-level lease check and re-checks each chat only while the pass is settled; the first refused/failed bookkeeping write stops bookkeeping for the remainder of the pass while every chat is still opened and read (
structured-agent-session-restart-restore.ts). - Restored tabs are projected, not written.
replaceStructuredAgentSessionTabis now synchronous and project-only (the/clearcommit already moved the tab), and the runtime startup projects restored chats then seeds the tab index once, only when it is absent (seedStructuredAgentSessionTabIndex,showSessionTabs). - Tests. New host and runtime suites pin the reported-once behavior, the byte-identical read-only store, the per-startup-step lock-wait bound for 1/4/8 chats, and the legacy one-shot seed. Verified locally:
pnpm tc:nodepasses and the four new suites pass (22 tests).
ℹ️ The legacy-seed limitation is slightly broader than the description states
The PR body says the deferred legacy limitation needs "a legacy profile, a failed seed and a later tab write in the same session," and that a later write "creates the list with only its own tab." A close is a counterexample: setSessionTabVisibility(..., false) on a store whose index was absent creates a non-null empty tab table, which serializes as sessionTabs: [] / visibleSessionIds: []. The next launch then sees present: true with sessionIds: [], sets targets = [], and restores none of the never-seeded chats — not just the closed one. The body also says main shares this limitation, but this one-shot seed is new here: on main a failed restored-tab write rejected startup outright.
Technical details
# Legacy seed failure followed by a close drops every unseeded chat
## Affected sites
- `src/main/runtime/agent-session-tab-table.ts:117-132` — a `hide` on a null table creates an empty, non-null `sessionTabs`.
- `src/main/runtime/agent-session-store-serialization.ts:17-19` — a non-null table serializes `sessionTabs: []` and `visibleSessionIds: []`.
- `src/main/runtime/agent-session-record-store.ts:132-135` + `orca-runtime-restore-structured-agent-session-tabs-once.ts:63` — next launch reports `present: true, sessionIds: []`, so `targets = []`.
- `src/main/runtime/structured-agent-session-tab-index-seed.ts:21-33` — the seed can only fire when the index is absent, which it no longer is.
## Required outcome
Only that the PR description be corrected (or a tracking note filed) so the acknowledged limitation is stated accurately: the triggering write may be a close, which drops all unseeded chats, and this behavior is new rather than shared with main.
## Suggested approach (optional)
No code change required for this PR; the seed design is deliberate and the fix is already deferred.DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
…up chat read The lease bookkeeping failure reporter called the host's failure sink directly, so a sink that threw turned a reported, recoverable store failure back into a rejected startup reconcile or read restore. The reporter now catches a sink throw and logs both the original failure and the sink error with console.warn.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- A throwing failure sink can no longer fail the startup read.
reportEachFailureOncenow invokesdeps.onLeaseReconcileFailureinside atry/catch; a throw falls back toconsole.warncarrying both the store failure and the sink error, soreconcileRestartLeasesandrestoreReadableSessionsstill resolve (structured-agent-session-restart-reconcile.ts:65-73). - Two host cases pin the invariant. A throwing sink over a held store lock is driven through both entrypoints, asserting each call resolves, the sink is called once, and the fallback warning is emitted (
structured-agent-session-startup-reconcile-failure.test.ts:198-220).
This closes the earlier P1 finding that a throwing error callback could escape the reader reconcile and re-trigger the "Session restore failed" path. onLeaseReconcileFailure is only ever called through reportEachFailureOnce (verified by grep), so the guard covers every path, including the recovery-resolution rejection handler in structured-agent-session-reveal.ts.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏

ELI5
If Orca could not save its chat bookkeeping file while starting up, the whole app went into "Session restore failed. Changes won't be saved until restart", even though only chat bookkeeping had failed. For example, the file was locked, could not be written, or was written by a newer Orca. Now the failure is logged, the app starts normally, and the chat tabs you had open come back with their history. Each chat redoes the bookkeeping the next time you send to it.
What Changed
The problem. Orca keeps a record file,
agent-sessions.json, that says which process may write to each chat (its lease) and which chat tabs are open. Startup reads chats in two steps, and several bookkeeping writes to that file sat inside them. When one of those writes failed, the error reached the renderer's startupcatch. That catch runsrecoverFromDegradedStartup, which shows the sticky "Session restore failed. Changes won't be saved until restart" toast and saves no tabs, layout or workspace state for the rest of the session.app:prepareTerminalStartupRestorationre-checks every saved chat's lease and writes the result (reconcileRestartLeases). A failed write rejected the step.session.tabs.listAll). Before reading each open chat, it ran four more writes, and each one rethrew a failure:On main, these can make those writes fail:
EACCESorEIOwhen saving;execution_owner_reconciling(how often this happens has not been measured);agent_session_legacy_required, even a write that changes nothing.No user report ties the banner to this path yet; this was found by reading the code. The newer-Orca case is not reachable from any released build today, because no released build has written a record file newer than schema 2. It becomes reachable once a later change moves these records into the chat database, where a downgrade after that change would open read-only.
Before: any of the failures above put the whole app into "Session restore failed", and nothing you changed in that session was saved until you restarted.
After:
[structured-agent-session] reconciling chat leases failed), or passed to the host's error sink when one is installed.Still not fixed (separate, deferred): a failure when the record file is first opened at startup still puts the app into "Session restore failed". That covers:
EACCES/EIOwhile opening the file;Opening happens before the lease check, in the host install, and failing startup on an install error is deliberate existing behavior that a test pins. Changing it needs its own decision.
The mechanism. The rule: on the two startup read steps, no write to the record file can fail the read. Writes the user asks for keep failing loudly: creating, revealing, attaching to or sending to a chat, stopping it, and committing a /clear.
createStructuredAgentSessionHostRestorebuilds both bookkeeping calls, and both the startup check and the chat restore use them.createReaderReconcilenever throws and answers whether every lease is settled.onLeaseReconcileFailure, and the next attach or send resolves it again, strictly.reportEachFailureOncelogs each distinct failure once until a check succeeds, so a failing store does not log on every chat list.publishStructuredAgentSessionTab(the strict write, used by create and reveal) now calls a newprojectStructuredAgentSessionTab, which only updates the runtime's in-memory tab snapshot.visibilityWriteMayFailand its per-tab warning from the previous commit are gone.store.showSessionTabs. It is never left half-written, which the next launch would read as complete and use to drop the other chats. If it fails, the list stays absent, and the next launch seeds it again unless another tab write lands first in the same session. That write can be opening, closing or clearing a chat, and it creates the list with only its own tab. The chats that were never seeded then do not reopen on the next launch, though History can still reopen them. This is a known, rare limitation that main shares: it needs a legacy profile, a failed seed and a later tab write in the same session.Why
A lease check, a recovery resolution and a tab-visibility record are bookkeeping. They must never decide whether the app can save your tabs and layout, or whether you can read a chat. Reading grants no writer, so a read has nothing to wait for.
Alternatives considered:
How this compares with other agent servers. What matches: a failed startup bookkeeping phase is logged and startup continues, and listing and reading chats never fail because of that bookkeeping. The differences:
agent-sessions.jsonin the middle of a restore: resolving recovery can stop a process, so it must act on a current answer. That case no longer happens. Packaged Orca and the background daemon (orcad) each refuse a second process on the same profile or data folder, and since feat(native-chat): one structured-chat journal database per host, owned by one process #23613 dev builds do too. refactor(native-chat): keep agent-session records in the chat journal database #24006 (still open) then moves these records into the chat database, which one process owns, and stops the record store from using the file lock. The common pattern elsewhere settles ownership once, in one startup phase run by the single server process. One design holds incoming commands behind a readiness gate until that phase finishes. Another runs one recovery sweep at startup that logs its failures, plus periodic sweeps. Follow-up, after refactor(native-chat): keep agent-session records in the chat journal database #24006 lands: replace the per-chat check with one startup reconcile phase, and keep the per-chat check that already runs before a chat's agent starts (a send, for example, first checks that chat's lease, is refused if the check cannot settle, and resolves that chat's recovery) as the per-chat form of that readiness gate. No ticket is filed yet. This PR keeps the existing behaviour on purpose, to stay a focused fix. Until then, on a healthy startup the check needs no lock when nothing changed, and after a failure it is skipped for the rest of that restore.Linked Issue
None. Found by code reading while planning the move of chat records into the chat database. This is the first, independent step of that plan: the later change makes a read-only record store reachable after a downgrade, and it relies on this step so that such a store never degrades startup.
Visual Proof
Live QA ran on a separate Mac (M4Air). Each launch used its own isolated profile whose
agent-sessions.jsonhad been written by a newer Orca (schema 3), so the store opens read-only and every write to it is refused.Native chat on, with a chat tab open at quit.
Before, on main (7980ab9): the "Session restore failed" toast appears and the chat tab is missing. Only Terminal 1 comes back.
After, on f3ec1a1 (behaviour identical to the current head; later commits change comments only), same profile plus a seeded chat history: no toast, and the Claude Chat tab is restored with its history.
After, on f3ec1a1: a Terminal 2 tab opened during that session is back after quit and relaunch, so saving still works. The record file was byte-identical after both launches.
Native chat off (record file only), empty profile.
Before, on main (7980ab9): the toast appears.
After, on 98a2e27 (the first commit of this PR; this off-mode pair was not re-run on later commits, but the f3ec1a1 run above goes through the same startup step and also shows no toast): no toast.
Testing
New tests:
structured-agent-session-startup-tab-restore.test.ts(runtime). These run over realagent-sessions.jsonfiles and real chat histories, throughrestoreStructuredAgentSessionTabs, the restore behindsession.tabs.listAll:structured-agent-session-startup-reconcile-failure.test.ts(host), six tests:structured-agent-session-restart-restore.test.ts, two new unit tests over 8 chats. When the pass-level lease check fails, no chat checks again or resolves recovery, and all 8 are opened. When recovery writes fail, no more start after the first failure: at most the 4 already running.orca-runtime-structured-session-restore.test.ts, new test: replacing a cleared chat's tab projects it and never callssetSessionTabVisibility.structured-agent-session-startup-reconcile.test.ts(runtime), unchanged in substance: startup prepare over a newer store resolves and reports; with no sink it logs.Ablations, each run against those files, with the red logs kept:
After merging main, on head 4e5c839, a run of 52 named test files passes, 369 tests: the new tests above plus the existing suites. The existing suites cover the host, the restart check and restore, crash and stale-turn recovery, the conversation commands, the tab table, the session-tabs RPCs, the runtime restore, host refusal, and the
tests/e2eunit suites for reopened tabs, owner status and cross-version wire.pnpm tc:nodepasses.AI Disclosure
Review
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
replacesSessionId. Newer profiles already publish that on main, so clients already handle it.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)