[P2] fix(runtime): stop broadcasting terminalSideEffects to clients without consumers (#11619)

* fix(runtime): stop broadcasting terminalSideEffects to clients without consumers

Co-authored-by: Orca <help@stably.ai>

* fix(runtime): keep mobile subscribers counted for side-effect availability

Excluding phones from the consumer-availability count added a new flip edge
(last desktop client leaving a phone-attached host), and the flip's tracker
rebuild cancels armed stale-working-title timers — stranding a 'working'
spinner on the phone. Availability counts all subscribers again; the
broadcast fix stays in the per-listener fan-out skip, now applied inside the
delivery callback so live-Set unsubscribe semantics and allocation-free
iteration are preserved.

Co-authored-by: Orca <help@stably.ai>

* fix(runtime): separate mobile title tracking from side-effect scans

---------

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-30 17:58:09 -07:00
committed by GitHub
co-authored by Orca
parent 6442a9f649
commit eb35c7fa3e
5 changed files with 240 additions and 48 deletions
+75
View File
@@ -8468,6 +8468,81 @@ describe('OrcaRuntimeService', () => {
expect(events).toHaveLength(1)
})
it('omits terminalSideEffects from non-consuming listeners while other events still flow', () => {
const runtime = new OrcaRuntimeService(store)
const desktopEvents: RuntimeClientEvent[] = []
const mobileEvents: RuntimeClientEvent[] = []
runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] })
runtime.onClientEvent((event) => desktopEvents.push(event))
runtime.onClientEvent((event) => mobileEvents.push(event), {
consumesTerminalSideEffects: false
})
runtime.onPtyData('pty-remote', '\x1b]0;Codex working\x07', 100)
runtime.notifyBranchRenamed(TEST_REPO_ID)
expect(desktopEvents.map((event) => event.type)).toEqual([
'terminalSideEffects',
'worktreesChanged'
])
expect(mobileEvents.map((event) => event.type)).toEqual(['worktreesChanged'])
})
it('keeps a phone-only host producing title state without emitting batches to it', async () => {
vi.useFakeTimers()
try {
const ptyId = `${TEST_REPO_ID}::/tmp/worktree-a@@pty-a`
const runtime = new OrcaRuntimeService(store)
const mobileEvents: RuntimeClientEvent[] = []
const trackerEntries = (
runtime as unknown as {
ptyTitleTrackersByPtyId: Map<string, { commandCodeDetector: unknown }>
}
).ptyTitleTrackersByPtyId
runtime.setPtyController({
write: () => true,
kill: () => true,
getForegroundProcess: async () => null,
listProcesses: async () => [{ id: ptyId, cwd: '/tmp/worktree-a', title: 'shell' }]
})
runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] })
runtime.onClientEvent((event) => mobileEvents.push(event), {
consumesTerminalSideEffects: false
})
const unsubscribeDesktop = runtime.onClientEvent(() => {})
runtime.onPtyData(ptyId, '\x1b]0;Codex working\x07', 100)
runtime.onPtyData(ptyId, 'output without a title\r\n', 101)
// The phone is still subscribed: disposing trackers on this edge would cancel
// its armed stale-working-title timer and strand a 'working' spinner (#1437).
unsubscribeDesktop()
await vi.advanceTimersByTimeAsync(3_000)
expect(trackerEntries.has(ptyId)).toBe(true)
expect(trackerEntries.get(ptyId)?.commandCodeDetector).toBeNull()
expect((await runtime.listTerminals()).terminals[0]).toMatchObject({ title: 'Codex' })
expect(mobileEvents.some((event) => event.type === 'terminalSideEffects')).toBe(false)
} finally {
vi.useRealTimers()
}
})
it('skips a listener unsubscribed mid-fan-out even with mobile exclusions active', () => {
const runtime = new OrcaRuntimeService(store)
const lateEvents: RuntimeClientEvent[] = []
runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] })
runtime.onClientEvent(() => {}, { consumesTerminalSideEffects: false })
runtime.onClientEvent(() => {
unsubscribeLate()
})
const unsubscribeLate = runtime.onClientEvent((event) => lateEvents.push(event))
runtime.onPtyData('pty-remote', '\x1b]0;Codex working\x07', 100)
expect(lateEvents).toEqual([])
})
it('emits one batched event per chunk with facts in byte order and attribution', () => {
const { runtime, batches } = createSideEffectRuntime()
syncSinglePty(runtime)