Skip to content

fix: isolate session tabs and synchronize contextual panels - #2578

Open
Nikhil (shadowfax92) wants to merge 6 commits into
mainfrom
fix/session-panel-sync
Open

Nikhil (shadowfax92) wants to merge 6 commits into
mainfrom
fix/session-panel-sync

Conversation

@shadowfax92

Copy link
Copy Markdown
Contributor

Summary

Opening or reading a tab could replace another panel's conversation, and heartbeat reconciliation could reopen panels the user had closed. Agent-created tabs now join their conversation immediately, share its live chat, and preserve unrelated panels and drafts.

  • Make submission and tab creation the explicit ownership boundaries; group created tabs even when the remaining tool script fails.
  • Bind contextual panels to stable native tab IDs, acknowledge successful opens per run, and keep manual panel selection independent of shared execution.
  • Pin hydration, replay, and Stop to the intended run; recover interrupted views and reconcile a rejected concurrent submission with the winning run.

Design

The server owns conversation execution and tab membership. One background broker projects assignments into browser session storage and performs native panel operations. Each panel owns its local selection and subscribes to an exact run; changing views only detaches that subscriber. The native toggle wrapper checks remembered tab state and uses explicit open/close operations. This follows the ownership separation in the archived MCP service while retaining the centralized run architecture from #2485.

Validation

  • bun run test:all, workspace lint, and workspace typecheck pass.
  • Real HTTP/AI SDK regressions cover reconnect failures, terminal output recovery, delayed cleanup, and run identity; server/browser seam tests cover ownership, grouping, and visibility.
  • BrowserOS 0.50.3 on macOS with a deterministic local provider: two concurrent sessions, grouped created tabs, live follow-up turns, unrelated closed tabs and drafts, manual close through heartbeat and worker restart, and New Chat while siblings continue.
  • Real panel input routing and Stop; forced simultaneous submissions verify the 409 loser joins the winning live run. An injected incognito window flag verifies completion uses the updated history policy.
  • Three independent review rounds; all nine verified findings addressed. Chromium source was used for reference; no Chromium changes.

@github-actions github-actions Bot added the fix label Sep 6, 2026
@shadowfax92

Copy link
Copy Markdown
Contributor Author

Greptile (@greptileai) review

@shadowfax92

Copy link
Copy Markdown
Contributor Author

Claude (@claude) review

@claude

claude Bot commented Sep 6, 2026

Copy link
Copy Markdown

Claude finished Nikhil (@shadowfax92)'s task in 4m 32s —— View job


Claude finished reviewing

Todo list

  • Gather context (diff, changed files, CLAUDE.md)
  • Review background/broker/panel ownership changes
  • Review server-side chat/run/tab-group changes
  • Review tests for adequacy
  • Post final review feedback

Summary

This is a well-structured refactor: run-pinned ActiveConversationRun capabilities, run-id-scoped /state//stream//stop (409 on stale run), and the explicit recordCreatedTabs/ownsTab split all close real races described in the PR body. Tests are thorough for the ownership/grouping scenarios. Two issues stood out that look like real regressions rather than intentional tradeoffs:


1. "New Chat" no longer cleans up the abandoned server-side conversation

chat-session.hooks.tsresetConversationState() (around line 892) used to call discardServerSession(previousConversationId), which sent DELETE /chat/:conversationId (with keepalive: true) to drop the in-memory ConversationRuns record and the persisted session/history row. That helper and its call site are gone in this PR, and I couldn't find any replacement caller of DELETE /chat/:conversationId anywhere in apps/app (grepped for discardServerSession, deleteSession, keepalive — no hits outside the still-existing server route/service).

The new comment in the diff explains the intent — "Stop and Delete are explicit shared execution/history operations, never lifecycle cleanup for a renderer" — which makes sense given a conversation can now be shared across panels/tabs and a renderer resetting shouldn't yank state out from under a sibling. But the result is that every "New Chat" click (including ones where the user never sent a message) now leaks:

  • an entry in ConversationRuns.runs (full message + chunk history) for the life of the server process — there's no TTL/sweep anywhere in conversation-runs.ts or main.ts, only the explicit delete() called from deleteSession.
  • a persisted row in the session/history store that previously would've been discarded.

If this is intentional (e.g., relying on some other lifecycle boundary not in this diff, or accepting the memory growth), it'd help to note it in the PR description; otherwise this looks like it needs a replacement cleanup path — e.g., only auto-delete when the abandoned conversation has no other live panel owner (which the server's panelByTab map could now answer).

Fix this →


2. attachConversationRun retries forever with no backoff, spamming Sentry on a permanently stale run

conversation-run-attachment.ts (lines 29-85): the while (!signal.aborted) loop retries every retryMs (default 1000ms) indefinitely whenever fetchConversationRunState/resumeStream fails, calling input.onError?.(error) on every iteration. In chat-session.hooks.ts, onError unconditionally calls sentry.captureException.

This differs from the previous behavior, which reported the error once per relevant event (tab activation/watch change) via attachedPanelRunRef, not on a fixed timer. Now, if a panel is assigned a runId that can never resolve — most plausibly after a full server/worker restart, since ConversationRuns is in-memory only and the broker's reconcile() explicitly retains stale "running" views across a reconnect ("Retain the last selected conversation when the server restarts or releases membership") — the /state?runId=... call will keep 409ing forever, and the panel will poll once a second and report to Sentry once a second for as long as it stays mounted (which could be indefinitely for a backgrounded tab).

Worth adding either an attempt cap / exponential backoff, or a terminal path that clears the stale view (e.g., falls back to a fresh conversation) after N consecutive failures, so a permanently-invalid run doesn't retry/report forever.

Fix this →


Minor

  • conversation-tab-groups.ts (lines ~404-412): the retry backoff 250 * 2 ** attempt is hardcoded rather than using @browseros/shared/constants/timeouts, which packages/browseros-agent/CLAUDE.md asks contributors to prefer for magic timing values. Not a bug, just a convention nit.
  • Everything else — the run-pinned recordCreatedTabs/ownsTab split in conversation-runs.ts, the immediate (non-deferred) recordCreatedPage commit in browser-tool-dispatch.ts (so a later script failure can't orphan a created tab), and the replayMessages vs messages separation — looks correct and is well covered by the new tests (e.g. conversation-runs.test.ts's "does not let automatic tab discovery steal another conversation origin", browser-mcp-module.test.ts's "assigns and groups a created tab while its script is still running, even if it later fails").

I was not able to run bun run test:all/lint/typecheck in this environment (no build/test execution was performed) — this review is based on static reading of the diff only.
· branch: fix/session-panel-sync

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

❌ Tests failed: 2/1224 failed

Ran 8 of 16 suites (8 not affected by this change).

Suite Passed Failed Skipped
server-agent 193/193 0 0
server-api 261/261 0 0
server-tools 248/248 0 0
server-browser 10/10 0 0
server-integration 10/10 0 0
server-lib 170/170 0 0
server-root 38/41 0 3
agent 289/291 2 0
claw-app n/a n/a not affected
claw-onboard n/a n/a not affected
app-onboard n/a n/a not affected
build n/a n/a not affected
release n/a n/a not affected
claw-server-rust n/a n/a not affected
claw-server-rust-quality n/a n/a not affected
claw-mcp n/a n/a not affected
Failed tests
  • agent (unnamed)
  • agent (unnamed)

View workflow run

@greptile-apps

greptile-apps Bot commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR separates contextual-panel selection from server-owned execution, pins attachment and stop operations to exact runs, and assigns only submitting or agent-created tabs to conversations.

  • Adds stable native tab identities and serialized panel open/close operations.
  • Introduces exact-run hydration, replay, cancellation, and concurrent-submission reconciliation.
  • Moves created-tab assignment and grouping to the moment browser creation is observed.
  • Adds per-tab search handoff and conditional panel-owner release.
  • Regresses restoration of cloud-only history in signed-in side panels by suppressing the cloud fetch for panel selections.

Confidence Score: 4/5

The PR is not safe to merge until signed-in side panels can restore cloud-only conversations selected from history.

Panel selections now disable the cloud-history query and fall back to a local-only restoration path, causing cloud-only conversations to open without their messages.

Files Needing Attention: packages/browseros-agent/apps/app/modules/chat/chat-session.hooks.ts

Important Files Changed

Filename Overview
packages/browseros-agent/apps/app/modules/chat/chat-session.hooks.ts Reworks chat ownership, exact-run attachment, stop behavior, and panel restoration; the new initialConversationId guards break cloud-only history restoration.
packages/browseros-agent/apps/app/modules/chat/panel-conversation.hooks.ts Resolves stable contextual or window-panel selections and persists manual navigation.
packages/browseros-agent/apps/app/lib/browseros/conversationPanelBroker.ts Serializes reconciliation and selection while preserving acknowledged visibility and manual panel state.
packages/browseros-agent/apps/app/lib/browseros/toggleSidePanel.ts Adds stable per-tab panel hosts and serializes explicit open/close operations.
packages/browseros-agent/apps/server/src/api/services/conversation-runs.ts Adds exact-run replay state, conditional panel release, and created-tab ownership.
packages/browseros-agent/apps/server/src/api/services/mcp/browser-tool-dispatch.ts Records and groups created pages immediately rather than applying post-execution touched-tab effects.
packages/browseros-agent/apps/server/src/api/routes/chat.ts Adds run-pinned state, stream, and stop behavior plus trusted panel-release routing.

Sequence Diagram

sequenceDiagram
  participant Panel as Contextual panel
  participant Broker as Background broker
  participant Server as Chat server
  participant Run as Conversation run
  participant Browser as Browser/MCP
  Panel->>Server: POST /chat with panelTabId
  Server->>Run: Start exact run and assign submitting tab
  Run-->>Broker: SSE panel assignment with runId
  Broker->>Panel: Store selected conversation/run
  Panel->>Server: Read exact-run state and replay stream
  Run->>Browser: Execute browser tools
  Browser-->>Run: Created tab observed
  Run-->>Broker: Assign created tab immediately
  Broker->>Panel: Open acknowledged contextual panel
Loading
Prompt To Fix All With AI
### Issue 1
packages/browseros-agent/apps/app/modules/chat/chat-session.hooks.ts:659-660
**Cloud history opens empty**

When a signed-in user selects a cloud-only conversation from side-panel history, the panel passes it as `initialConversationId`. This condition disables the GraphQL history query and instead tries to restore the conversation from the local server. Cloud-only conversations are not stored locally, so no messages are restored and the selected conversation opens as an empty chat.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "fix(chat): reconcile rejected submission..." | Re-trigger Greptile

Comment on lines +659 to +660
enabled:
!!conversationIdParam && isLoggedIn && !options?.initialConversationId,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Cloud history opens empty

When a signed-in user selects a cloud-only conversation from side-panel history, the panel passes it as initialConversationId. This condition disables the GraphQL history query and instead tries to restore the conversation from the local server. Cloud-only conversations are not stored locally, so no messages are restored and the selected conversation opens as an empty chat.

Knowledge Base Used: BrowserOS extension client

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/browseros-agent/apps/app/modules/chat/chat-session.hooks.ts
Line: 659-660

Comment:
**Cloud history opens empty**

When a signed-in user selects a cloud-only conversation from side-panel history, the panel passes it as `initialConversationId`. This condition disables the GraphQL history query and instead tries to restore the conversation from the local server. Cloud-only conversations are not stored locally, so no messages are restored and the selected conversation opens as an empty chat.

**Knowledge Base Used:** [BrowserOS extension client](https://app.greptile.com/browseros-org-2/-/custom-context/knowledge-base/browseros-ai/browseros/-/docs/browseros-extension-client.md)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant