From 1158930a04fbadebcda21b703ec626f6bbe5b3ab Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Wed, 19 Aug 2026 19:24:03 -0400 Subject: [PATCH] fix(browser): count a stream ending as page use so the idle clock restarts then MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Streaming pins a page but frames are not activity, so a client that only watched for longer than the idle window had the page parked on the next 30s recheck after closing the pane — not 5 minutes later. Restamp the page's activity when the stream pin lifts (clearPageGate), the same markHeadlessBrowserPageUsed the subscribe path already routes through. At that instant the page is guaranteed resident (the pin vetoed any park, and pin-delete -> touch is synchronous), so the wake is a pure timestamp; a page closed or crashed mid-stream has no record left and the call no-ops. Download and certificate pins deliberately do not get the same treatment: a finished download implies no viewer to come back, and a resolved challenge is always followed by commands that stamp activity themselves. --- docs/reference/headless-linux-server.md | 3 +- ...a-runtime-browser-parked-targeting.test.ts | 30 +++++++++++++++++++ src/main/runtime/orca-runtime-browser.ts | 6 ++++ 3 files changed, 38 insertions(+), 1 deletion(-) diff --git a/docs/reference/headless-linux-server.md b/docs/reference/headless-linux-server.md index afa59ab4e1d..0b7fce6881d 100644 --- a/docs/reference/headless-linux-server.md +++ b/docs/reference/headless-linux-server.md @@ -293,7 +293,8 @@ storage live in the profile partition and survive, but an unsubmitted form or an SPA's in-memory state does not. A page is never parked while a paired client is streaming it, while a command against it is in flight, while its initial load is still running, or while it is waiting on a certificate -decision. A navigation that has still not finished by the load timeout is +decision. A stream ending counts as use, so a viewer who closes the pane gets +the full idle window before the page parks, even if they only watched. A navigation that has still not finished by the load timeout is deliberately reclaimable — otherwise one stalled page per create could hold a renderer forever, which is the failure this exists to prevent; waking it simply retries the address. Parking never closes a page: the page stays open until diff --git a/src/main/runtime/orca-runtime-browser-parked-targeting.test.ts b/src/main/runtime/orca-runtime-browser-parked-targeting.test.ts index cfa08ab89e4..68c3436156c 100644 --- a/src/main/runtime/orca-runtime-browser-parked-targeting.test.ts +++ b/src/main/runtime/orca-runtime-browser-parked-targeting.test.ts @@ -233,6 +233,36 @@ describe('headless parked-page targeting', () => { expect(fake.wakeCalls).toEqual([]) }) + it('counts a stream ending as page use so the idle clock restarts then', async () => { + // Why: streaming pins the page but frames are not activity, so a client + // that only watched for longer than the idle window would otherwise have + // the page parked ~one recheck after closing the pane. + const { RuntimeBrowserCommands } = await import('./orca-runtime-browser') + const { startBrowserScreencast } = await import('../browser/browser-screencast-stream') + let resolveDone!: () => void + const done = new Promise((resolve) => { + resolveDone = resolve + }) + vi.mocked(startBrowserScreencast).mockResolvedValue({ + stop: vi.fn(() => resolveDone()), + done + } as never) + const fake = createFakeHeadlessHost(['live-a'], []) + const commands = new RuntimeBrowserCommands(fake.host) + + const started = await commands.browserScreencast( + { worktree: 'id:wt-1', page: 'live-a', format: 'jpeg' }, + { sendBinary: vi.fn() } + ) + const wakesBeforeStop = fake.wakeCalls.length + + started.session.stop() + await started.session.done + + await vi.waitFor(() => expect(fake.wakeCalls.length).toBeGreaterThan(wakesBeforeStop)) + expect(fake.wakeCalls.at(-1)).toBe('live-a') + }) + it('reports a listed-index overrun against the merged listing', async () => { const { RuntimeBrowserCommands } = await import('./orca-runtime-browser') const fake = createFakeHeadlessHost(['live-a'], ['parked-b']) diff --git a/src/main/runtime/orca-runtime-browser.ts b/src/main/runtime/orca-runtime-browser.ts index 84bf8389b9f..763a745f34c 100644 --- a/src/main/runtime/orca-runtime-browser.ts +++ b/src/main/runtime/orca-runtime-browser.ts @@ -578,6 +578,12 @@ export class RuntimeBrowserCommands { let stoppingPromise: Promise | null = null const clearPageGate = (): void => { this.activeScreencastPageIds.delete(browserPageId) + // Why (STA-4341): the stream pin kept this page resident while a client + // watched without issuing commands, so its activity stamp can be + // arbitrarily stale the instant the pin lifts — stale enough to park it + // on the next 30s recheck. Viewing counts as use: restamp so the idle + // window runs from when the viewer left, not from their last command. + void this.markHeadlessBrowserPageUsed(browserPageId).catch(() => {}) if (this.activeScreencastsByPageId.get(browserPageId) === activeRecord) { this.activeScreencastsByPageId.delete(browserPageId) }