Repository navigation
fix(native-chat): every journal append reaches the chats that are open - #22811
Conversation
No client ever called agentSession.requestHandoff or mounted the handoff chrome. Delete the handoff coordinator, the terminal-owner runtime, the proof write path and the unmounted UI. Keep agentSession.handoffStatus, which released desktop clients read for worktree activation, and let records an older build left mid handoff reconcile through the ordinary restart and recovery paths.
Eviction now drains delivered events before quit's resume-offer snapshot. An unbounded wait there sits ahead of the provider stop, so a sink whose journal write stalls kept the child running until the step deadline aborted the eviction. The offer is advisory: bound the drain and stop the child regardless. Co-Authored-By: Claude <noreply@anthropic.com>
`claudeAuthEnvCarriedForward`, `isPathWithinDirectory` and
`queryWindowsProcessRowsFresh` lost their last caller with the handoff. The
fresh-scan tests now go through `queryWindowsProcessDescendants({ fresh: true })`,
the teardown path that still depends on that contract.
Co-Authored-By: Claude <noreply@anthropic.com>
Six comments still named the handoff coordinator, a handoff suspend, or a terminal-owned session as live participants in the flows they describe. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ement The removed restart handoff test pinned this branch; nothing else did. Co-Authored-By: Claude <noreply@anthropic.com>
The handoff removal dropped the per-session queue from `handoffStatus`, so a read landing mid-start reported the reservation (no owner) instead of the settled chat owner, and shipped desktop clients blocked worktree activation on it. The read is queued again, as it was before the removal. Co-Authored-By: Claude <noreply@anthropic.com>
The gate only refused a write when a PTY had been bound to a chat session, and the only code that ever bound one was the terminal handoff this branch removes. With it gone, every admit/readmit returned "admitted" unconditionally, so the checks on the renderer write path, the runtime controller backstop, terminal.send, agent prompts, preview input and orchestration pointers, the refusal fields on terminal.send and worker-start receipts, the plugin and CLI refusal copy, and the adopted-pane orchestration routing could no longer run. Ordinary writes take the same path in the same order as before. Co-Authored-By: Claude <noreply@anthropic.com>
…alled appendLegacyTranscriptMessages fed the terminal transcript catch-up and proveClaudeTranscriptBranch backed the terminal owner's exit proof. Both lost their last caller with the handoff. Their tests now go through the live entry points instead: the roster bounds through the legacy import, the pinned-read and growth tests through the ancestry replay the history window uses, and the marker rules through the string proof in their own file rather than the session-file resolver's. Co-Authored-By: Claude <noreply@anthropic.com>
A send refused because the chat's owner is not settled showed "The session is mid-handoff (<stage>)." in the composer. With the handoff gone, the stages that reach it are a chat that is still starting, or one whose previous agent process has not yet been confirmed stopped. The message now says which of the two it is. The refusal code is unchanged. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
With the terminal handoff gone, the module named codex-tui-rollout-proof holds only the pinned rollout lookup that structured Codex launches use to resume a thread, so the name described code that no longer exists. Rename the module and its options type. Also drop a mobile allowlist assertion that pinned the removed agentSession.requestHandoff method, which no longer exists to allow.
The handoffStatus reply type still listed the terminal handoff's fields and states (terminal placement, host label, proof retry, queued and waiting phases, the to-terminal direction). No host writes them any more and the only client reader parses the reply as unknown, so they described nothing. The reply on the wire is unchanged.
…t decode Nothing in this build writes a terminal owner (`runtimeKind: 'tui'`) or the handoff's `preparing` / `old-owner-stopped` stages, but the in-memory types still admitted them, so readers across the host kept branches for values no path produces and the compiler could not point at them. The store now validates the on-disk shape, which still accepts those values so an older record is not quarantined, and maps them once while parsing: - `preparing` and `old-owner-stopped` become `recovering` - a `tui` lease becomes `native`; when it records a process it also becomes `conflicted`, the claim every build probes but never stops. A plain native owner would be stopped by restart recovery, here and in older builds. Revisions are taken over the normalized state on both sides of every compare, and the mapped record reaches disk with the store's first transaction, the same way the tab-id backfill does. The in-memory types narrow to what this build writes, and the branches that existed only for the removed values go. Structured-worker identity keeps its verdict for a former terminal owner by refusing a conflicted claim rather than a non-native kind.
…ation A reservation only ever names a native owner now, so the request no longer carries a kind and the reserved lease records `native` directly. The attach params keep `runtimeKind`: agentSession.ensure and create accept it, and the operation fingerprint stored in the ledger covers it.
…at changes nothing else Hiding a tab also committed the visibility index, so the no-op transaction wrote the file even when its open-time revision was wrong. Committing the index first leaves the pending rewrite as the only reason to write.
A journal write and its delivery to open readers were two calls, and some writers made only the first. A failed start whose lease could not be handed back, a provider revision with no frame behind it, and eviction's settlement were all journaled without reaching an open chat. A journal handle now reports every durable change, and the host's session map binds that report to the session's readers when the handle is set. Writers no longer publish what they append; the per-writer publish calls are deleted.
…dle enters only through the map
59dd098 to
427dc8d
Compare
There was a problem hiding this comment.
ℹ️ No blocking issues — one minor hardening suggestion inline.
Reviewed changes — Reviewed the full 16-file diff at 59dd098 (2 commits) plus the surrounding journal store, host lifetime, subscriber, and send-settlement code.
- One commit listener for every durable change —
AgentSessionJournal.observeCommitsfires from the rowcommitcallback and the epochadoptcallback, so appends, revisions, and epoch replacements share one notification edge. - Delivery bound when a handle becomes a session's journal — the new
StructuredAgentSessionConversationsMapsubclass binds that listener inset, coalesces a synchronous burst with aqueuedflag, and delivers one microtask later only if the map still holds that exact handle. - Explicit publishes after appends removed — 17
publish/ctx.publish()calls acrossturns,turns-prompt,restart-continuation,send-preparation,host-mutations,thread-goal, andconversation-command. Each was verified to follow a commit on the in-map handle, so the new delivery covers it. Publishes that republish non-journal state (options, rewind phase, fence, attach adoption) are deliberately kept. - Tests — host-level append-delivery tests (failed start with refused lease release, unpublished provider revision, direct append), conversation-delivery unit tests with replaced/dropped negative controls and a reader-failure case, and a retargeted surface-lifetime assertion for the eviction
unknownsettlement.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — incremental delta since the prior pullfrog review at 59dd098 (the branch was rebased; 4484e4c/59dd098 became 6dad39a/97bf3bb with identical content, then 427dc8d landed).
readonly journalinvariant —StructuredAgentSessionHostSession.journalis nowreadonlywith a comment, so a handle can only enter a session through the map'sset, which is the only place delivery is bound. The one test that swapped a handle in place now builds a new session and callssessions.set(structured-agent-session-journal-handles.test.ts:245). This closes the hardening gap from the prior review.- Exactly-once coverage —
liveReadernow counts batches, and two tests assert a provider-streamed row whose frame also publishes arrives exactly once, and a host append followed by an explicit publish emits a single batch (the second publish sends nothing). - Plain-
Mapbinding test — a handlesetthrough aMap-typed reference still binds delivery, pinning the fact that collaborators holding the map asMapkeep the behavior.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Each commit now delivers itself, so the publish a provider frame still sends afterwards found every reader caught up but still read rows and rebuilt the timeline for each one. A caught-up reader now skips the read.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes — incremental delta since the prior pullfrog review at 427dc8d (2 commits: 50d2e33, bd72b13).
- Caught-up fast path —
delivernow returns through a newemitCaughtUphelper without reading rows when the subscriber's cursor already equals the journal's. It guards on!journal.isReadOnly, so a read-only journal still takes itsschema_unreadablereset; the other resets (epoch_changed,cursor_ahead,cursor_compacted) all imply cursors differ, so none is masked. - In-loop caught-up branch unified — the existing
!advancedbranch calls the same helper. Its batch cursor changes frompage.window.nextCursortosubscriber.cursor, which are equal in that branch, so the checkpoint/activity/commands behavior is unchanged. - Coverage — the provider-frame exactly-once test now spies
journal.readSinceand asserts a single read, pinning that the frame's publish behind the commit's own delivery reads nothing.bd72b13adds only anoxlint-disableSAFETY comment to the teardown test's fake-journal cast.
The prior review's one inline suggestion (make journal readonly so a handle can only enter through set) is addressed in 427dc8d; that thread is resolved. The delivery and append-delivery suites pass locally (11 tests).
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
|
Live Electron check of this PR. After: Setup: a hidden Orca dev build from a fresh profile, driven over the Chrome DevTools Protocol, with screenshots taken from that hidden window. A. Failure row reaches the open chat: PASS in After. The Before build did not reproduce the gap. With the flag present, opening a Claude chat showed one "The provider stopped before it finished starting: … not signed in (rig)." row. Sending Row counts, shown / written to disk:
Each reopen adds one row because reopening tries to start Claude again, and that start writes its own failure row. No row was ever missing or shown twice. The Before build already showed these rows live. This setup's failure happens on a fresh chat whose session hand-back succeeds, and that path already notified the open chat before this PR. The cases this PR fixes (the session hand-back itself failing, a revision written with no frame after it, the settlement written on eviction) are not reached by this setup. So A shows no regression and that each row arrives exactly once, even though a row now has two notifiers (the new commit listener and the frame publish). It does not show the before/after difference. B. Normal chat: PASS. With the flag removed, C. Compaction status row: PASS for delivery. This PR removed the explicit notifications that followed compaction, so this row now reaches the open chat only through the new commit listener. D. Quit and relaunch with the chat open: PASS. I quit the app while the chat was open and relaunched it on the same profile. The chat came back as the active tab, and the full transcript appeared about 0.9 s after I attached. Watched every 150 ms after that, it never blanked, reset or duplicated. Claude resumed the same session, and Separately, an untouched "Terminal 1" tab disappears after reloading the window. This happens on both the Before ( |
Review status: ready for review at
|
…rnal-append-delivery # Conflicts: # src/main/native-chat/agent-session-wire/structured-agent-session-subscribers.ts # src/main/runtime/agent-session-record-store-legacy-handoff-lease.test.ts # src/renderer/src/lib/worktree-agent-activation-gate.test.ts
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 📝 WalkthroughWalkthroughThe host now observes journal commits and schedules delivery to session subscribers. Subscriber delivery skips catch-up reads when its cursor matches the writable journal cursor. Journal-writing paths no longer explicitly publish after appends; the publish contract now covers state held outside the journal. Priority: ⬇️ Low Merge Risk: 🟡 Moderate · up to A reader failure could produce an uncaught process error if error reporting also fails. Contain reporting failures before merging. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change is limited to an experimental chat mode that is off by default. Session ownership checks and replay during journal replacement limit the apparent exposure, but some failure and authorization paths 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 💡 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.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 84e9de58-37bc-41b0-b251-b91b27b08449
📒 Files selected for processing (19)
src/main/native-chat/agent-session-journal/journal-store.tssrc/main/native-chat/agent-session-wire/structured-agent-session-append-delivery.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-conversations.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-conversations.tssrc/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.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-journal-handles.test.tssrc/main/native-chat/agent-session-wire/structured-agent-session-restart-continuation.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-wiring.tssrc/main/native-chat/agent-session-wire/structured-agent-session-send-preparation.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-thread-goal.tssrc/main/native-chat/agent-session-wire/structured-agent-session-turns-prompt.tssrc/main/native-chat/agent-session-wire/structured-agent-session-turns.tssrc/main/native-chat/agent-session-wire/structured-agent-session-unanswered-dispatch-release.test.tssrc/main/native-chat/agent-session-wire/structured-conversation-command.ts
💤 Files with no reviewable changes (4)
- src/main/native-chat/agent-session-wire/structured-agent-session-restart-resume-host.ts
- src/main/native-chat/agent-session-wire/structured-agent-session-turns-prompt.ts
- src/main/native-chat/agent-session-wire/structured-agent-session-thread-goal.ts
- src/main/native-chat/agent-session-wire/structured-conversation-command.ts
Included review availability: Your plan provides up to 10 included reviews per hour; 0 remain after this review.
| try { | ||
| this.delivery.deliver(sessionId, journal) | ||
| } catch (error) { | ||
| this.delivery.onDeliveryError(sessionId, error) |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Contain failures from onDeliveryError.
If deliver throws and onDeliveryError also throws, the exception escapes the delivery microtask. A reader failure can then become an uncaught process error despite the durable write succeeding. Catch failures from the error handler within this microtask and report them through a non-throwing fallback.
|
Ready to merge. This is my view as coordinator of the native-chat redesign PRs.
|









Scope: which chat this affects
experimentalStructuredNativeChatsetting, which is off by default (src/shared/default-global-settings.ts:137).Stacked on #22783.
ELI5
When Orca writes a new row into a chat's history (an error message, a status note, a settled message), it also has to tell the open chat windows about it. Those were two separate steps, and a few places did only the first. The row was saved but the open chat never showed it until you reopened the chat. Now saving a row is what tells the open windows, so no code path can forget.
What Changed
Before:
After: every change that commits to an open chat's journal reaches that chat's open readers, including:
The row shows up without the writer doing anything more.
Mechanism:
observeCommits. It fires after a row commits and after an epoch is replaced.StructuredAgentSessionConversations. When a journal handle is set into the map as a session's journal, the map binds that listener to the session's existing publish. That publish delivers new rows to subscribers and updates the status feed, the send-settlement waiters and the turn-completion feed.awaitresumes after the delivery, so ordering is the same as the explicit publish it replaces. A reader that throws is reported throughonEventSinkErrorand cannot fail a write that is already durable.journalfield is nowreadonly, so a new handle can only reach a session throughset, andsetbinds delivery. That holds even for collaborators that hold the map as a plainMap. The one test that swapped a handle in place now goes throughset.structured-agent-session-subscribers.ts, theadvancedcheck and the cursor update after each batch). So the second call finds nothing new and sends nothing. A subscriber already at the journal's cursor is answered without reading any rows, so that second call costs no journal read or timeline rebuild.awaitreturns: before any RPC reply to a send, prompt answer,setOption, goal, compaction or rewind, and before the restart continuation continues. Code that reads the journal right after a mutation reads the handle's in-memory state, which the commit has already updated.Deleted: every host-side publish that followed an append:
turns.ts);publishsurface.Kept on purpose, because these republish state held outside the journal:
publish, used whensetOptionchanges the record's options and when rewind changes the record's rewind phase (both feed the status projection);publishFence, a snapshot when the fence moves;Why
The bug class is a writer that forgets the second call. It has already shipped twice:
if (released).The next planned change adds six more append sites. Adding the missing publish at each known site would leave the class open.
The alternatives:
awaitwould no longer see delivery done.Differences from the common pattern:
Linked Issue
Native-chat redesign, step E: every journal append reaches open readers. It must land before the delivery-loop change, which adds the new append sites.
Visual Proof
N/A: this is host-side delivery. The fix is proven at the subscriber in unit tests (below). A before/after screen would need a lease-release failure injected into a running app.
Testing
New tests. Each asserts what a live subscriber receives, not what the journal holds.
structured-agent-session-append-delivery.test.ts, through the real host:if (released)path.structured-agent-session-conversations.test.ts:structured-agent-session-append-delivery.test.ts:structured-agent-session-conversations.test.ts: a handle set through a plainMapreference is still bound.structured-agent-session-surface-lifetime.test.ts: a compatibility send-wait on a chat that is evicted now answers with theunknownsettlement that eviction journaled, instead of nothing. Before this change that settlement never reached anyone.Ablations. Each was run by deleting lines from the committed file, then restoring the file:
Deleting the map's
observeCommitsbinding turns all 7 delivery tests red, with an empty subscriber or an uncalleddeliver. The two "delivers nothing" tests are negative controls and stay green.Deleting the row-commit notification turns the 6 row tests red.
Deleting the epoch-adopt notification turns the epoch test red.
Deleting the replaced/dropped handle check turns those 2 tests red, because
deliveris called.Deleting the subscriber's cursor update after a batch turns both exactly-once tests red: each row arrives twice.
Deleting the caught-up check turns the provider-frame test red: the rows are read twice.
The
readonlyguard is enforced by the type checker. The old in-place swap instructured-agent-session-journal-handles.test.tsfailstscwith TS2540.Base arm: with every source file restored to refactor(native-chat): remove the unused terminal handoff #22783's head, 4 tests are red:
With the lease-release failure removed, the failed-start test is green at the base. So its red is exactly the missing delivery when the release fails.
Suites:
src/main/native-chat: all 176 files and 1478 tests pass after rebasing onto refactor(native-chat): remove the unused terminal handoff #22783's latest head (713b126feb). Two earlier full runs each hit one timeout under load (lease-renewer, thenrefusal-retry); each file passes alone.src/main/claude,src/main/codex,src/main/runtimeandsrc/main/ipc: pass. The only failures are the known local ones:claude-structured-real-cli;CODEX_LB_API_KEYis set.tests/e2e/cross-version-wire: 64 of 65 pass.release-checkout.unit.test.tstimes out while checking out the baseline release. That is disk and git time, unrelated to this change, and it passed on one of three runs.Checks:
pnpm tc:node,pnpm tc:web,oxlinton the changed files andcheck:code-quality:changedare clean.Platforms: macOS locally.
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
unknownsettlement instead of the pending result. The client already handles that state, and would have read it on its next subscribe.publish/tryPublishafter their appends, and the resolved-append…AndPublishvariants still exist.Author: @BrennanKB5
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)