From c9e620bd2b6a241a2a38eefa0733c6a72dc5aab1 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Fri, 10 Jul 2026 23:23:01 -0700 Subject: [PATCH] fix(terminal): exclude passive mobile query responders --- docs/reference/terminal-query-authority.md | 10 +++++----- src/main/runtime/mobile-presence-lock.test.ts | 16 ++++++++++++++++ src/main/runtime/orca-runtime.ts | 6 +++++- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/reference/terminal-query-authority.md b/docs/reference/terminal-query-authority.md index 90e8511f106..ad712998820 100644 --- a/docs/reference/terminal-query-authority.md +++ b/docs/reference/terminal-query-authority.md @@ -104,11 +104,11 @@ arrived while the initial snapshot was being built and its output sequence is covered by that snapshot, runtime RPC re-emits only the bounded query sequence after the snapshot; ordinary covered output stays deduplicated. `terminal.send` accepts the resulting `inputKind: query-reply` only from the earliest active -mobile subscriber while mobile owns the terminal driver. Earliest is determined -by preserved `subscribedAt`, so a soft-leave resubscribe does not change reply -ownership. Peer phones are rejected, the next subscriber is promoted on -unsubscribe, and desktop parser and capability handlers stay silent until -desktop retakes the driver. +phone-fitted mobile subscriber while mobile owns the terminal driver. Earliest +is determined by preserved `subscribedAt`, so a soft-leave resubscribe does not +change reply ownership; passive desktop-mode watchers are excluded. Peer phones +are rejected, the next subscriber is promoted on unsubscribe, and desktop +parser and capability handlers stay silent until desktop retakes the driver. Everything the emulator emits outside a forwarding window is discarded, which also swallows unsolicited core emissions (e.g. native 997 color-scheme pushes diff --git a/src/main/runtime/mobile-presence-lock.test.ts b/src/main/runtime/mobile-presence-lock.test.ts index 015823f3a98..6213765bb20 100644 --- a/src/main/runtime/mobile-presence-lock.test.ts +++ b/src/main/runtime/mobile-presence-lock.test.ts @@ -271,6 +271,22 @@ describe('mobile presence lock — multi-mobile semantics', () => { expect(runtime.isMobileTerminalQueryReplyAuthority('pty-1', 'phone-B')).toBe(false) }) + it('excludes an older passive desktop-mode subscriber from reply authority', async () => { + const { runtime } = createRuntime() + runtime.setMobileDisplayMode('pty-1', 'desktop') + await runtime.handleMobileSubscribe('pty-1', 'phone-A', { cols: 45, rows: 20 }) + await vi.advanceTimersByTimeAsync(10) + await runtime.handleMobileSubscribe('pty-1', 'phone-B', { cols: 38, rows: 18 }) + + await vi.advanceTimersByTimeAsync(10) + runtime.markMobileActor('pty-1', 'phone-B') + runtime.setMobileDisplayMode('pty-1', 'auto') + await runtime.applyMobileDisplayMode('pty-1') + + expect(runtime.isMobileTerminalQueryReplyAuthority('pty-1', 'phone-A')).toBe(false) + expect(runtime.isMobileTerminalQueryReplyAuthority('pty-1', 'phone-B')).toBe(true) + }) + it('most-recent actor wins active phone-fit dims (B subscribes after A)', async () => { const { runtime, ptySizes } = createRuntime() await runtime.handleMobileSubscribe('pty-1', 'phone-A', { cols: 45, rows: 20 }) diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 2a206acf32b..a4e3e64a272 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -6487,9 +6487,13 @@ export class OrcaRuntimeService { return false } // Why: soft-leave resubscribe preserves the original subscription time but - // reinserts the record. Elect from that stable age, not mutable Map order. + // reinserts the record. Elect fitted responders from that stable age, not + // mutable Map order or passive desktop-mode watchers. let earliest: { clientId: string; subscribedAt: number } | null = null for (const subscriber of subscribers.values()) { + if (!subscriber.wasResizedToPhone) { + continue + } if (earliest === null || subscriber.subscribedAt < earliest.subscribedAt) { earliest = subscriber }