Repository navigation
fix(native-chat): a chat's finished-turn alerts come from the host recorded on its tab, not its workspace id - #25081
Conversation
…space-id collision no longer hides its owner
…amped on its tab, not its workspace id
…same rule as the chat pane
…ntion-bridge-tab-host # Conflicts: # src/renderer/src/components/native-chat/StructuredAgentSessionAttentionBridge.test.tsx
…ntion-bridge-tab-host
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. 🧰 Additional context used📚 Code guidelines (1)No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configuration
📒 Files selected for processing (3)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 WalkthroughWalkthroughThe attention bridge now resolves a session owner from the tab and derives a runtime target from that owner. It renders the attention subscriber only when a target exists. Tests cover paired and local chats with colliding workspace IDs, an unrecorded chat that later records its host, and restoration of a mirrored tab’s execution host ID. Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to No actionable merge-blocking risk is identified for this change; it is mergeable after normal checks. Security Architecture ReviewSecurity architecture risk: 🔵 Low · up to The change ties completion alerts to the host recorded on the chat tab rather than a potentially colliding workspace ID. No new permission or trust-boundary bypass was identified. Remote authorization and duplicate-event delivery guarantees were not fully verified. 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
- Attention bridge follows the tab's recorded host.
StructuredAgentSessionAttentionnow resolves its runtime target viastructuredAgentSessionOwnerForTab+structuredAgentSessionTargetForHostinstead ofgetRuntimeEnvironmentIdForWorktree+getActiveRuntimeTarget, matching the chat pane andStructuredAgentSessionStatusBridge.tsx:204-212. The subscribed body moves intoStructuredAgentSessionOwnedAttention. - No owner means no subscription. A tab whose workspace id is ambiguous on an unselected workspace (or whose host no runtime serves) now holds no subscription, where it previously fell back to local. This is the same posture the status bridge and chat pane already take.
- Tests. New
it.eachcollision test (recorded paired tab stays onenvironment:env-1and delivers with either workspace selected), a local-chat-under-paired-collision test, a rewritten unrecorded-tab test, and a mirrored-tab test pinning the host stamp on first publish.
Verified: both changed test files pass (26 tests), and oxlint is clean on the three changed files. The production change is a strict reuse of the shared owner rule, so the three readers now agree by construction.
deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

ELI5
A chat running on a paired Orca server can share its workspace id with a workspace on this computer. When that happens and you're looking at a different workspace, the desktop stops listening for that chat's "turn finished" events. You get no notification and no unread dot when the chat finishes. This PR makes the desktop listen to the host the chat actually runs on, which the chat tab already records.
What Changed
The problem (reproduced). Take a structured chat mirrored from a paired Orca server, in workspace
repo1::/tmp/wt. A workspace with the same id then appears on this computer. Worktree ids arerepoId::path, so two hosts can produce the same one. Now select any other workspace. The desktop drops its subscription to the paired server's turn-completion stream and subscribes to this computer instead. When the paired chat finishes a turn, nothing lights up and no notification is sent. The same mistake happens the other way round: a chat on this computer gets moved to the paired server's stream while that server's same-id workspace is selected.Why it happened. The attention bridge decides which host to listen to by looking up the tab's workspace id (
getRuntimeEnvironmentIdForWorktree). When two hosts have that id, the lookup can only answer through the selected workspace. With another workspace selected it returns nothing, and the bridge falls back to this computer. #24205 gave every structured chat tab a record of the host it was launched on or mirrored from (executionHostId). The chat pane, the status bridge and tab close all read that owner through one function,structuredAgentSessionOwnerForTab. It returns the recorded host, and falls back to the workspace only for a tab without a recorded host, refusing an ambiguous id. The attention bridge was the one reader with its own rule.The fix. The attention bridge now asks
structuredAgentSessionOwnerForTaband maps the answer withstructuredAgentSessionTargetForHost, exactly as the status bridge and the chat pane do. Its own lookup is deleted, so every reader of a chat agrees on which host owns it. When no host can be named, the bridge holds no subscription, as the chat pane and status bridge already do.Because the bridge now uses the shared rule, it also changes how it treats a tab with no recorded host whose workspace id two hosts share while that workspace isn't selected. Before, the bridge guessed this computer. Now it waits until the tab's host is recorded. The test for that case, added in #24518, changes accordingly. The wait is short: every structured chat tab gets its host recorded when it is launched (
structured-agent-session-provisional-tab.ts:63) or the first time a host publishes it (terminal-surfaces.ts:126). That publish path is used by paired servers and by this computer's own chat sync (local-structured-session-tabs-sync/snapshot-apply.ts:149). A new test checks that a tab restored from before hosts were recorded gets its host on that first publish.A tab with no recorded host whose workspace is the one selected keeps working exactly as before. Main has a test for this case: a paired chat whose tab was restored from before hosts were recorded, with a same-id workspace appearing on this computer while the paired workspace is selected. It must stay subscribed to the paired server and deliver the completion. That test is kept unchanged and passes with this PR, because the shared rule names the selected workspace's host when the id is ambiguous. Only the case where another workspace is selected changes, as described above.
What users see. A chat's finished-turn notification, unread dot and tab marker now arrive whichever workspace you have selected, including when a paired server and this computer share a workspace id. Nothing changes when workspace ids don't collide.
Why
One rule for "who owns this chat" means the chat pane, its status and its alerts can't follow different hosts. Before this change, during a collision the chat pane could read one host while the alerts listened to another. Reusing the shared function removes this bug class from the bridge rather than adding another guard for the colliding case.
Alternatives considered:
Linked Issue
None. Found while fixing a red test on main (that fix has since landed on main separately).
Visual Proof
Checked in the real app on Windows, before (main
752871bd678) and after (this PR,d1169d8ba06). The chat tab in this check has its host recorded, the normal case. That check is still valid for the current head: the only change since is a merge of main, which leaves the bridge code fromd1169d8ba06unchanged. The desktop app is paired to a second Orca running as a headless server on the same machine. A structured Codex chat runs on that server, using a stand-in Codex so no real agent or login is involved. The desktop is given a workspace of its own with the same id as the server's workspace, the way the repo's end-to-end tests set up a collision. The chat is sent a turn that takes about a minute, and another workspace is selected while it runs. Desktop notifications were switched off, so the unread marker is the signal.Before (main): the server finished the turn, but the desktop never heard about it. The paired workspace shows no unread bell, and the desktop recorded no unread completion.
The chat really did finish on the server (full reply, "Worked for 1m 1s"):
After (this PR): with another workspace selected, the paired workspace shows the amber unread bell ("Mark as read"), and the desktop recorded the chat's completion as unread.
Selecting the empty workspace opens a Claude chat there on its own, in both builds. That is existing behaviour, unrelated to this change. On this test machine it fails to start, because no Claude is signed in.
Testing
I manually tested these changes locally
Automated tests added/updated, or explained why not below
StructuredAgentSessionAttentionBridge.test.tsx:mirrored-agent-tab-label.test.ts: a tab restored without a recorded host gets one on its first publish.Ablation:
Related files: the bridge test,
structured-attention-dispatch.test.ts,notification-sources.test.ts,StructuredAgentSessionStatusBridge.test.tsx,ai-vault-session-resume-in-chat-workspace.test.tsandmirrored-agent-tab-label.test.tsgive 91 passed.Manual: the before/after check in the real app on Windows, described under Visual Proof.
Web typecheck (
tc:web),oxlintandoxfmton the changed files, the changed-code quality gate and the React Doctor changed-files check pass.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
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)