Skip to content

fix(mobile): stop host streams after relay subscription cancellation - #18926

Merged
nwparker merged 2 commits into
mainfrom
nwparker/perf-astra-mobile-relay-cancel
Sep 6, 2026
Merged

nwparker merged 2 commits into
mainfrom
nwparker/perf-astra-mobile-relay-cancel

Conversation

@nwparker

@nwparker nwparker commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​398 0 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​398
Prod 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​111 $\color{#cf222e}{\Huge{\mathbf{−}}}$​17 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​94

ELI5

Leaving a mobile browser view could stop showing its pictures without telling the host to stop making and sending them. The relay client built the wrong browser unsubscribe method and forgot subscriptions cancelled before the host returned their cleanup ID. Tab and native-chat subscriptions also missed their existing cleanup RPCs.

Reuse the existing unsubscribe builders, retain only cleanup routing metadata while an already-sent subscription becomes ready, and release listeners on relay failure. Tab cleanup uses the original request ID so one subscriber cannot stop another subscriber to the same workspace; early tab cancellation waits for the host's initial snapshot/cleanup registration. Cancelling an unsent request sends nothing.

Measurements

Production MobileRelayRpcStreams connected to the production RuntimeBrowserScreencastController, with a controlled frame producer and in-memory transport. After cancellation the producer attempts 100 frames of 65,536 bytes if it is still running. This is a deterministic resource budget, not a measured network rate or device battery estimate.

Resource after cancellation Before After
Retained host browser subscriptions 1 0
Frames sent during the next 100 frame opportunities 100 0
Bytes sent during those opportunities 6,553,600 0

Both cancellation timings reproduce: after ready, and before the client receives ready. The checked-in budget tests fail against original mobile-relay-rpc-streams.ts from d8c4c830639bf3d603c9250a536e0323ca0a7a19, reporting all three before counts, and pass with this change.

Reproduce: pnpm --dir mobile test src/transport/mobile-relay-browser-cancel-budget.test.ts. Run the same test with only that production stream file restored from the baseline to reproduce the failure.

Validation

39 mobile stream/session/cancellation tests pass in the final focused run, including 100 late-ready cancellation cycles, exactly-once cleanup, unsent requests, error/end/completion/disconnect, native-chat/terminal cleanup identities, and sibling tab subscriptions. Three existing host session-tab unsubscribe tests pass. Mobile typecheck, targeted lint, formatting, and diff checks pass.

Uses existing RPC methods and optional request-scoped cleanup fields; no new stream opcode, capability, or host schema. The existing parent revision's session-tab schema/handler accepts request-scoped cleanup. Execution stays with the owning host; cancellation unsubscribes the view and does not terminate remote PTYs or agent work. Folder workspaces use the same workspace selector path.

Visual proof: N/A — active-view rendering is unchanged; the regression is work continuing after its viewer has unsubscribed. The production-controller budget test verifies that cleanup stops that work. No foreground app or mobile simulator launched.

Negative user-facing trade-offs

None identified. Active subscriptions keep receiving their existing data and same-workspace sibling subscriptions remain active. Pending cancellation retains only request/method/cleanup identifiers until the reply, completion, or disconnect; it does not retain disposed view listeners or frame buffers. No polling delay, shortened output, or cache freshness change.

@coderabbitai

coderabbitai Bot commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 86c036da-a1be-473a-8491-fba620b5a145

📥 Commits

Reviewing files that changed from the base of the PR and between c58d7a0 and 33151f1.

📒 Files selected for processing (6)
  • mobile/src/transport/mobile-relay-browser-cancel-budget.test.ts
  • mobile/src/transport/mobile-relay-rpc-session.test.ts
  • mobile/src/transport/mobile-relay-rpc-session.ts
  • mobile/src/transport/mobile-relay-rpc-stream-cancellation.test.ts
  • mobile/src/transport/mobile-relay-rpc-streams.ts
  • mobile/src/transport/rpc-client-terminal-subscription.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 2 remain after this review.


📝 Walkthrough

Walkthrough

Mobile relay streams now track sent and snapshot states and maintain cancellation records. Cancellation builds request-specific unsubscribe payloads, defers cleanup until required responses arrive, ignores late responses, and preserves live sibling streams. Browser screencast cancellation tests verify host shutdown and zero frame sends. Session failure now clears streams before link closure, preventing late event delivery to released listeners.

Merge Risk: ⚪ Minimal · up to 33151

Mobile relay stream cancellation now releases host-side subscriptions after client unsubscribe while preserving active shared subscriptions. The covered cancellation, failure, and cleanup paths show no remaining merge-blocking risk.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed, relevant, and explains the behavior, implementation, measurements, testing, and visual-proof rationale. However, it omits the required Linked Issue entry and does not comp… Add a linked issue using the required Fixes #... format. Complete or explicitly mark the remaining required sections, including AI Disclosure, Review, Agent skill upstream boundary, Notes, and the Checklist.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 6 functions across 6 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: stopping host streams after mobile relay subscription cancellation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Description check

Explanation

The description is detailed, relevant, and explains the behavior, implementation, measurements, testing, and visual-proof rationale. However, it omits the required Linked Issue entry and does not complete several template sections or checklist items.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

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.

✅ No new issues found.

Reviewed changes

  • MobileRelayRpcStreams cancellation rewrite — cancel now builds the correct host unsubscribe per method, retains only a cleanup route ({ method, unsubscribe? }) in a cancelledSubscriptions map while an already-sent subscription awaits its server-assigned subscription id, and releases listeners on relay failure.
  • buildStreamUnsubscribe request-scoped identity — gains an optional requestId so session.tabs.unsubscribe carries subscriptionId and evicts only the originating subscriber (not a same-worktree sibling); the direct client's 2-arg call is unchanged.
  • stream.sent / receivedSnapshot gates — unsent requests never send an unsubscribe; a pre-snapshot session.tabs cancel defers its unsubscribe until the host's initial snapshot resolves cleanup registration.
  • fail() clears streams — mobile-relay-rpc-session.ts now calls streams.clear() on failure, matching the close() path so disposed listeners never fire on a late response.

The fix is well-scoped and the tests are strong: exact-equality assertions (not loose truthiness), a deterministic production-controller budget test that pins retained subscriptions/frames/bytes to zero after cancellation for both ready and late-ready timings, sibling-tab isolation, and the repeated late-ready cycle. I verified the host side accepts each cleanup shape (session.tabs.unsubscribe request-scoped subscriptionId, nativeChat.unsubscribe, browser.screencast.unsubscribe/runtime.clientEvents.unsubscribe), and the only wire change is an optional field on an existing method, which is compatible. No blocking or minor issues to raise.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

nativeChat and terminal unsubscribe tokens are deterministic per view target,
and the host evicts on duplicate registration. Skip the unsubscribe RPC while a
live sibling on the same connection still owns that token.

@pullfrog pullfrog Bot left a comment

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.

✅ No new issues found.

Reviewed changes

This delta adds a sibling-preservation guard on top of the prior cancellation rewrite, so cancelling one stream no longer evicts a live sibling that shares a deterministic host cleanup token.

  • sendUnsubscribe / hasLiveOwner guard — before emitting an unsubscribe, the client now checks whether a live, sent sibling derives the same { method, params } token and skips the frame if so, keeping nativeChat (agent:sessionId) and terminal (terminal:clientId) sibling registrations alive until the last subscriber unmounts.
  • buildParamsUnsubscribe extraction — unifies terminal + generic unsubscribe derivation behind one helper, with a new StreamUnsubscribe type; the deferred session.tabs path and browser.screencast/runtime.clientEvents paths are unchanged.
  • Sibling tests — new it.each coverage for nativeChat/terminal sibling preservation plus the unsent-sibling case, asserting the unsubscribe is still sent when the would-be owner was never sent to the host.

The guard is correctly scoped: session.tabs never false-matches because its unsubscribe embeds the request-scoped subscriptionId, and browser.screencast/runtime.clientEvents never match because buildStreamUnsubscribe returns null for them (their cleanup uses a unique server-assigned id). Unsent siblings are excluded, matching the fact that the host has no cleanup registration to evict. I traced the token shapes through buildTerminalUnsubscribeParams and buildNativeChatUnsubscribe and confirmed the JSON.stringify comparison is deterministic for these fixed param shapes.

Pullfrog  | View workflow run | Using DeepSeek Pro (free via Pullfrog for OSS) | 𝕏

@nwparker
nwparker merged commit 09ee4c1 into main Sep 6, 2026
21 checks passed
enrick-dev pushed a commit to enrick-dev/orca that referenced this pull request Oct 15, 2026
…tablyai#18926)

* fix(mobile): release cancelled relay stream subscriptions

* fix(mobile): keep shared-token relay siblings live on unsubscribe

nativeChat and terminal unsubscribe tokens are deterministic per view target,
and the host evicts on duplicate registration. Skip the unsubscribe RPC while a
live sibling on the same connection still owns that token.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant