fix(terminal): exclude passive mobile query responders

This commit is contained in:
Neil
2026-07-11 01:53:03 -07:00
parent ecc511951c
commit c9e620bd2b
3 changed files with 26 additions and 6 deletions
+5 -5
View File
@@ -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
@@ -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 })
+5 -1
View File
@@ -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
}