fix(browser): count a stream ending as page use so the idle clock restarts then

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.
This commit is contained in:
Jinwoo-H
2026-08-19 19:24:03 -04:00
parent 2c479aabd7
commit 1158930a04
3 changed files with 38 additions and 1 deletions
+2 -1
View File
@@ -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
@@ -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<void>((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'])
+6
View File
@@ -578,6 +578,12 @@ export class RuntimeBrowserCommands {
let stoppingPromise: Promise<void> | 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)
}